From 028ddfe290b6b49eeef270659d61f05ec5da59b6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 4 Feb 2015 19:24:59 +1100 Subject: [PATCH 01/38] Starts adding asp.net identity --- .../src/common/services/user.service.js | 8 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 27 +++++- src/Umbraco.Web.UI/packages.config | 8 ++ .../Security/Identity/AppBuilderExtensions.cs | 91 +++++++++++++++++++ .../Security/Identity/OwinExtensions.cs | 19 ++++ .../UmbracoBackOfficeAuthenticationHandler.cs | 87 ++++++++++++++++++ ...bracoBackOfficeAuthenticationMiddleware.cs | 26 ++++++ .../UmbracoBackOfficeAuthenticationOptions.cs | 18 ++++ src/Umbraco.Web/Umbraco.Web.csproj | 26 ++++++ src/Umbraco.Web/packages.config | 7 ++ 10 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs create mode 100644 src/Umbraco.Web/Security/Identity/OwinExtensions.cs create mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs create mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs create mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs diff --git a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js index 85578dbb99..8d99086ba5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/user.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/user.service.js @@ -56,7 +56,7 @@ angular.module('umbraco.services') /** Method to count down the current user's timeout seconds, - this will continually count down their current remaining seconds every 2 seconds until + this will continually count down their current remaining seconds every 5 seconds until there are no more seconds remaining. */ function countdownUserTimeout() { @@ -64,8 +64,8 @@ angular.module('umbraco.services') $timeout(function () { if (currentUser) { - //countdown by 2 seconds since that is how long our timer is for. - currentUser.remainingAuthSeconds -= 2; + //countdown by 5 seconds since that is how long our timer is for. + currentUser.remainingAuthSeconds -= 5; //if there are more than 30 remaining seconds, recurse! if (currentUser.remainingAuthSeconds > 30) { @@ -128,7 +128,7 @@ angular.module('umbraco.services') } } } - }, 2000, //every 2 seconds + }, 5000, //every 5 seconds false); //false = do NOT execute a digest for every iteration } diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index f51d61b15c..c2de5441ab 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -151,7 +151,29 @@ False ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll + + ..\packages\Microsoft.AspNet.Identity.Core.2.1.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + + + ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + + False + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + + ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll + + + ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + + + ..\packages\Microsoft.Owin.Security.OAuth.2.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + ..\packages\Microsoft.Bcl.Async.1.0.165\lib\net45\Microsoft.Threading.Tasks.dll @@ -178,6 +200,9 @@ False ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll + + ..\packages\Owin.1.0\lib\net40\Owin.dll + System @@ -324,6 +349,7 @@ Properties\SolutionInfo.cs + loadStarterKits.ascx ASPXCodeBehind @@ -2518,7 +2544,6 @@ - diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index da11256606..150759f582 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -9,6 +9,8 @@ + + @@ -21,10 +23,16 @@ + + + + + + diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs new file mode 100644 index 0000000000..f2ef0a010d --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Web; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Microsoft.Owin.Extensions; +using Owin; +using Umbraco.Core; +using Umbraco.Core.Configuration; + +namespace Umbraco.Web.Security.Identity +{ + public static class AppBuilderExtensions + { + ///// + ///// Configure Identity User Manager for Umbraco + ///// + ///// + ///// + ///// + //public static void ConfigureUserManagerForUmbraco(this IAppBuilder app, ApplicationContext appContext) + // where T : UmbracoIdentityUser, new() + //{ + + // //Don't proceed if the app is not ready + // if (appContext.IsConfigured == false + // || appContext.DatabaseContext == null + // || appContext.DatabaseContext.IsDatabaseConfigured == false) return; + + // //Configure Umbraco user manager to be created per request + // app.CreatePerOwinContext>( + // (o, c) => UmbracoMembersUserManager.Create( + // o, c, ApplicationContext.Current.Services.MemberService)); + + // //Configure Umbraco member event handler to be created per request - this will ensure that the + // // external logins are kept in sync if members are deleted from Umbraco + // app.CreatePerOwinContext>((options, context) => new MembersEventHandler(context)); + + // //TODO: This is just for the mem leak fix + // app.CreatePerOwinContext, UmbracoMembersUserManager>>( + // (o, c) => new OwinContextDisposal, UmbracoMembersUserManager>(c)); + //} + + /// + /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline + /// + /// + /// + public static IAppBuilder UseUmbracoBackAuthentication(this IAppBuilder app) + { + if (app == null) throw new ArgumentNullException("app"); + + app.Use(typeof (UmbracoBackOfficeAuthenticationMiddleware), + //ctor params + app, new UmbracoBackOfficeAuthenticationOptions(), UmbracoConfig.For.UmbracoSettings().Security); + + app.UseStageMarker(PipelineStage.Authenticate); + return app; + } + + //This is a fix for OWIN mem leak! + //http://stackoverflow.com/questions/24378856/memory-leak-in-owin-appbuilderextensions/24819543#24819543 + private class OwinContextDisposal : IDisposable + where T1 : IDisposable + where T2 : IDisposable + { + private readonly List _disposables = new List(); + private bool _disposed = false; + + public OwinContextDisposal(IOwinContext owinContext) + { + if (HttpContext.Current == null) return; + + _disposables.Add(owinContext.Get()); + _disposables.Add(owinContext.Get()); + + HttpContext.Current.DisposeOnPipelineCompleted(this); + } + + public void Dispose() + { + if (_disposed) return; + foreach (var disposable in _disposables) + { + disposable.Dispose(); + } + _disposed = true; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/OwinExtensions.cs b/src/Umbraco.Web/Security/Identity/OwinExtensions.cs new file mode 100644 index 0000000000..4b83f97bd3 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/OwinExtensions.cs @@ -0,0 +1,19 @@ +using System.Web; +using Microsoft.Owin; + +namespace Umbraco.Web.Security.Identity +{ + internal static class OwinExtensions + { + /// + /// Nasty little hack to get httpcontextbase from an owin context + /// + /// + /// + public static HttpContextBase HttpContextFromOwinContext(this IOwinContext owinContext) + { + return owinContext.Get(typeof(HttpContextBase).FullName); + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs new file mode 100644 index 0000000000..1b0adc604c --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs @@ -0,0 +1,87 @@ +using System; +using System.Reflection; +using System.Threading.Tasks; +using System.Web.Security; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Infrastructure; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Security; +using Umbraco.Core; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Used to allow normal Umbraco back office authentication to work + /// + public class UmbracoBackOfficeAuthenticationHandler : AuthenticationHandler + { + private readonly ISecuritySection _securitySection; + + public UmbracoBackOfficeAuthenticationHandler(ISecuritySection securitySection) + { + _securitySection = securitySection; + } + + /// + /// Checks if we should authentication the request (i.e. is back office) and if so gets the forms auth ticket in the request + /// and returns an AuthenticationTicket based on that. + /// + /// + /// + /// It's worth noting that the UmbracoModule still executes and performs the authentication, however this also needs to execute + /// so that it assigns the new Principal object on the OWIN request: + /// http://brockallen.com/2013/10/27/host-authentication-and-web-api-with-owin-and-active-vs-passive-authentication-middleware/ + /// + protected override Task AuthenticateCoreAsync() + { + if (ShouldAuthRequest()) + { + var authTicket = GetAuthTicket(Request, _securitySection.AuthCookieName); + if (authTicket != null) + { + return Task.FromResult(new AuthenticationTicket(new UmbracoBackOfficeIdentity(authTicket), new AuthenticationProperties())); + } + } + + return Task.FromResult(null); + } + + private bool ShouldAuthRequest() + { + var httpContext = Context.HttpContextFromOwinContext(); + + // do not process if client-side request + if (httpContext.Request.Url.IsClientSideRequest()) + return false; + + return UmbracoModule.ShouldAuthenticateRequest(httpContext.Request, Request.Uri); + } + + /// + /// Returns the current FormsAuth ticket in the request + /// + /// + /// + /// + private static FormsAuthenticationTicket GetAuthTicket(IOwinRequest request, string cookieName) + { + if (request == null) throw new ArgumentNullException("request"); + + var formsCookie = request.Cookies[cookieName]; + if (formsCookie == null) + { + return null; + } + //get the ticket + try + { + return FormsAuthentication.Decrypt(formsCookie); + } + catch (Exception) + { + return null; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs new file mode 100644 index 0000000000..1275b72c08 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs @@ -0,0 +1,26 @@ +using Microsoft.Owin; +using Microsoft.Owin.Security.Infrastructure; +using Owin; +using Umbraco.Core.Configuration.UmbracoSettings; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Used to enable the normal Umbraco back office authentication to operate + /// + public class UmbracoBackOfficeAuthenticationMiddleware : AuthenticationMiddleware + { + private readonly ISecuritySection _securitySection; + + public UmbracoBackOfficeAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, UmbracoBackOfficeAuthenticationOptions options, ISecuritySection securitySection) + : base(next, options) + { + _securitySection = securitySection; + } + + protected override AuthenticationHandler CreateHandler() + { + return new UmbracoBackOfficeAuthenticationHandler(_securitySection); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs new file mode 100644 index 0000000000..c7609a8e83 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs @@ -0,0 +1,18 @@ +using Microsoft.Owin.Security; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Umbraco auth options - really just ensures that it is operating in Active mode + /// + public sealed class UmbracoBackOfficeAuthenticationOptions : AuthenticationOptions + { + public UmbracoBackOfficeAuthenticationOptions() + : base("UmbracoBackOffice") + { + //Must be active, this needs to look at each request to determine if it should execute, + // if set to passive this will not be the case + AuthenticationMode = AuthenticationMode.Active; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e261c048d3..45e0ce5fe2 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -131,7 +131,25 @@ False ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll + + ..\packages\Microsoft.AspNet.Identity.Core.2.1.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + + + ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + + ..\packages\Microsoft.Owin.2.1.0\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll + + + ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + + + ..\packages\Microsoft.Owin.Security.OAuth.2.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -148,6 +166,9 @@ False ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll + + ..\packages\Owin.1.0\lib\net40\Owin.dll + System @@ -518,6 +539,11 @@ + + + + + diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 294079e830..3fade0702d 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -6,6 +6,8 @@ + + @@ -17,9 +19,14 @@ + + + + + From 93df2edec2c247ef6615496ed75195580f7b71da Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 6 Feb 2015 13:47:00 +1100 Subject: [PATCH 02/38] Initial install which now uses Identity middleware to perform the back office auth (no longer done in our module). Created custom data secure classes that use the legacy Forms auth logic for backwards compat. This means that the cookie can still be written the old way and still auth the new way if required. Now need to clean a lot of this up. --- .../Security/AuthenticationExtensions.cs | 49 ++++- src/Umbraco.Core/Umbraco.Core.csproj | 6 + src/Umbraco.Core/packages.config | 2 + src/Umbraco.Tests/App.config | 8 + src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 61 ++++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 2 +- .../Editors/AuthenticationController.cs | 11 ++ .../Security/Identity/AppBuilderExtensions.cs | 8 +- .../FormsAuthenticationSecureDataFormat.cs | 62 ++++++ .../UmbracoBackOfficeAuthenticationHandler.cs | 179 ++++++++++++++++-- ...bracoBackOfficeAuthenticationMiddleware.cs | 17 +- .../UmbracoBackOfficeAuthenticationOptions.cs | 18 -- ...coBackOfficeCookieAuthenticationOptions.cs | 35 ++++ src/Umbraco.Web/Security/WebSecurity.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 8 +- src/Umbraco.Web/UmbracoModule.cs | 30 +-- src/Umbraco.Web/app.config | 4 + src/Umbraco.Web/packages.config | 2 +- src/umbraco.MacroEngines/app.config | 4 + src/umbraco.businesslogic/packages.config | 2 + .../umbraco.businesslogic.csproj | 6 + src/umbraco.editorControls/app.config | 4 + 22 files changed, 454 insertions(+), 66 deletions(-) create mode 100644 src/Umbraco.Web.UI/App_Code/OwinStartup.cs create mode 100644 src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs delete mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs create mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 8511d39125..49511697d7 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -1,10 +1,15 @@ using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Principal; using System.Threading; using System.Web; using System.Web.Security; +using Microsoft.Owin; using Newtonsoft.Json; using Umbraco.Core.Configuration; @@ -268,6 +273,23 @@ namespace Umbraco.Core.Security return new HttpContextWrapper(http).GetUmbracoAuthTicket(); } + internal static FormsAuthenticationTicket GetUmbracoAuthTicket(this IOwinContext ctx) + { + if (ctx == null) throw new ArgumentNullException("ctx"); + //get the ticket + try + { + return GetAuthTicket(ctx.Request.Cookies.ToDictionary(x => x.Key, x => x.Value), UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName); + } + catch (Exception) + { + //TODO: Do we need to do more here?? need to make sure that the forms cookie is gone, but is that + // taken care of in our custom middleware somehow? + ctx.Authentication.SignOut(); + return null; + } + } + /// /// This clears the forms authentication cookie /// @@ -301,16 +323,18 @@ namespace Umbraco.Core.Security private static FormsAuthenticationTicket GetAuthTicket(this HttpContextBase http, string cookieName) { - if (http == null) throw new ArgumentNullException("http"); - var formsCookie = http.Request.Cookies[cookieName]; - if (formsCookie == null) + var allKeys = new List(); + for (var i = 0; i < http.Request.Cookies.Keys.Count; i++) { - return null; + allKeys.Add(http.Request.Cookies.Keys.Get(i)); } + var asDictionary = allKeys.ToDictionary(key => key, key => http.Request.Cookies[key].Value); + //get the ticket try { - return FormsAuthentication.Decrypt(formsCookie.Value); + + return GetAuthTicket(asDictionary, cookieName); } catch (Exception) { @@ -320,6 +344,21 @@ namespace Umbraco.Core.Security } } + private static FormsAuthenticationTicket GetAuthTicket(IDictionary cookies, string cookieName) + { + if (cookies == null) throw new ArgumentNullException("cookies"); + + if (cookies.ContainsKey(cookieName) == false) return null; + + var formsCookie = cookies[cookieName]; + if (formsCookie == null) + { + return null; + } + //get the ticket + return FormsAuthentication.Decrypt(formsCookie); + } + /// /// Renews the forms authentication ticket & cookie /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 7cdb815933..9579bbba59 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -54,6 +54,9 @@ False ..\packages\log4net-mediumtrust.2.0.0\lib\log4net.dll + + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -70,6 +73,9 @@ False ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll + + ..\packages\Owin.1.0\lib\net40\Owin.dll + diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index f3f9b617f7..c13e24ef1a 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -10,10 +10,12 @@ + + \ No newline at end of file diff --git a/src/Umbraco.Tests/App.config b/src/Umbraco.Tests/App.config index 3d35ae5c93..86e6a0cbfe 100644 --- a/src/Umbraco.Tests/App.config +++ b/src/Umbraco.Tests/App.config @@ -152,6 +152,14 @@ + + + + + + + + diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs new file mode 100644 index 0000000000..e215fd552c --- /dev/null +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Microsoft.Owin.Security.Cookies; + +using Owin; +using Umbraco.Web.Security.Identity; +using Umbraco.Web.UI; + +[assembly: OwinStartup(typeof(OwinStartup))] + +namespace Umbraco.Web.UI +{ + + /// + /// Summary description for Startup + /// + public class OwinStartup + { + + public void Configuration(IAppBuilder app) + { + ////Single method to configure the Identity user manager for use with Umbraco + //app.ConfigureUserManagerForUmbraco(); + + //// Enable the application to use a cookie to store information for the + //// signed in user and to use a cookie to temporarily store information + //// about a user logging in with a third party login provider + //// Configure the sign in cookie + //app.UseCookieAuthentication(new CookieAuthenticationOptions + //{ + // AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, + + // Provider = new CookieAuthenticationProvider + // { + // // 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 = SecurityStampValidator + // .OnValidateIdentity, UmbracoApplicationUser, int>( + // TimeSpan.FromMinutes(30), + // (manager, user) => user.GenerateUserIdentityAsync(manager), + // identity => identity.GetUserId()) + // } + //}); + + //Ensure owin is configured for Umbraco back office authentication - this must + // be configured AFTER the standard UseCookieConfiguration above. + app.UseUmbracoBackAuthentication(); + + app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); + + } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index c2de5441ab..fb07ae1ff4 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2579,7 +2579,7 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True 7300 / - http://localhost:7300 + http://localhost:7301 False False diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 00498f7e61..e3f13d46f3 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -9,6 +9,9 @@ using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Security; using AutoMapper; +using Microsoft.AspNet.Identity; +using Microsoft.Owin; +using Microsoft.Owin.Security; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Membership; @@ -109,6 +112,14 @@ namespace Umbraco.Web.Editors //TODO: Clean up the int cast! var ticket = UmbracoContext.Security.PerformLogin(user); + //TODO: Normally we'd do something like this for identity, but we're mixing and matching legacy and new here + // so we'll keep the legacy way and move forward with this in our custom handler for now, eventually replacing + // the above legacy logic with the new stuff. + + //OwinContext.Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); + //OwinContext.Authentication.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, + // await user.GenerateUserIdentityAsync(UserManager)); + var http = this.TryGetHttpContext(); if (http.Success == false) { diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index f2ef0a010d..dc68a43152 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Owin.Extensions; using Owin; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; namespace Umbraco.Web.Security.Identity { @@ -52,7 +53,12 @@ namespace Umbraco.Web.Security.Identity app.Use(typeof (UmbracoBackOfficeAuthenticationMiddleware), //ctor params - app, new UmbracoBackOfficeAuthenticationOptions(), UmbracoConfig.For.UmbracoSettings().Security); + app, + new UmbracoBackOfficeCookieAuthenticationOptions( + UmbracoConfig.For.UmbracoSettings().Security, + GlobalSettings.TimeOutInMinutes, + GlobalSettings.UseSSL), + LoggerResolver.Current.Logger); app.UseStageMarker(PipelineStage.Authenticate); return app; diff --git a/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs b/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs new file mode 100644 index 0000000000..fac130f0ca --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs @@ -0,0 +1,62 @@ +using System; +using System.Web.Security; +using Microsoft.Owin.Security; +using Newtonsoft.Json; +using Umbraco.Core.Security; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Custom secure format that uses the old FormsAuthentication format + /// + internal class FormsAuthenticationSecureDataFormat : ISecureDataFormat + { + private readonly int _loginTimeoutMinutes; + + public FormsAuthenticationSecureDataFormat(int loginTimeoutMinutes) + { + _loginTimeoutMinutes = loginTimeoutMinutes; + } + + public string Protect(AuthenticationTicket data) + { + //TODO: Where to get the user data? + //var userDataString = JsonConvert.SerializeObject(userdata); + + var ticket = new FormsAuthenticationTicket( + 5, + data.Identity.Name, + data.Properties.IssuedUtc.HasValue ? data.Properties.IssuedUtc.Value.LocalDateTime : DateTime.Now, + data.Properties.ExpiresUtc.HasValue ? data.Properties.ExpiresUtc.Value.LocalDateTime : DateTime.Now.AddMinutes(_loginTimeoutMinutes), + data.Properties.IsPersistent, + "", //User data here!! This will come from the identity + "/" + ); + + return FormsAuthentication.Encrypt(ticket); + } + + public AuthenticationTicket Unprotect(string protectedText) + { + FormsAuthenticationTicket decrypt; + try + { + decrypt = FormsAuthentication.Decrypt(protectedText); + if (decrypt == null) return null; + } + catch (Exception) + { + return null; + } + + var identity = new UmbracoBackOfficeIdentity(decrypt); + + return new AuthenticationTicket(identity, new AuthenticationProperties + { + ExpiresUtc = decrypt.Expiration.ToUniversalTime(), + IssuedUtc = decrypt.IssueDate.ToUniversalTime(), + IsPersistent = decrypt.IsPersistent + }); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs index 1b0adc604c..3393075c14 100644 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs @@ -1,26 +1,35 @@ using System; using System.Reflection; +using System.Security.Principal; +using System.Threading; using System.Threading.Tasks; +using System.Web; using System.Web.Security; using Microsoft.Owin; using Microsoft.Owin.Security; +using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.Infrastructure; +using Newtonsoft.Json; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Security; using Umbraco.Core; +using Umbraco.Core.Logging; namespace Umbraco.Web.Security.Identity { /// /// Used to allow normal Umbraco back office authentication to work /// - public class UmbracoBackOfficeAuthenticationHandler : AuthenticationHandler + public class UmbracoBackOfficeAuthenticationHandler : AuthenticationHandler { - private readonly ISecuritySection _securitySection; + private readonly ILogger _logger; + private bool _shouldRenew; + private DateTimeOffset _renewIssuedUtc; + private DateTimeOffset _renewExpiresUtc; - public UmbracoBackOfficeAuthenticationHandler(ISecuritySection securitySection) + public UmbracoBackOfficeAuthenticationHandler(ILogger logger) { - _securitySection = securitySection; + _logger = logger; } /// @@ -33,24 +42,165 @@ namespace Umbraco.Web.Security.Identity /// so that it assigns the new Principal object on the OWIN request: /// http://brockallen.com/2013/10/27/host-authentication-and-web-api-with-owin-and-active-vs-passive-authentication-middleware/ /// - protected override Task AuthenticateCoreAsync() + protected override async Task AuthenticateCoreAsync() { if (ShouldAuthRequest()) { - var authTicket = GetAuthTicket(Request, _securitySection.AuthCookieName); - if (authTicket != null) + var ticket = GetAuthTicket(Request); + + if (ticket == null) { - return Task.FromResult(new AuthenticationTicket(new UmbracoBackOfficeIdentity(authTicket), new AuthenticationProperties())); + _logger.Warn(@"Unprotect ticket failed"); + return null; } + + DateTimeOffset currentUtc = Options.SystemClock.UtcNow; + DateTimeOffset? issuedUtc = ticket.Properties.IssuedUtc; + DateTimeOffset? expiresUtc = ticket.Properties.ExpiresUtc; + + if (expiresUtc != null && expiresUtc.Value < currentUtc) + { + return null; + } + + if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration) + { + TimeSpan timeElapsed = currentUtc.Subtract(issuedUtc.Value); + TimeSpan timeRemaining = expiresUtc.Value.Subtract(currentUtc); + + if (timeRemaining < timeElapsed) + { + _shouldRenew = true; + _renewIssuedUtc = currentUtc; + TimeSpan timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); + _renewExpiresUtc = currentUtc.Add(timeSpan); + } + } + + var context = new CookieValidateIdentityContext(Context, ticket, Options); + + await Options.Provider.ValidateIdentity(context); + + return new AuthenticationTicket(context.Identity, context.Properties); } - return Task.FromResult(null); + return await Task.FromResult(null); + } + + protected override async Task ApplyResponseGrantAsync() + { + AuthenticationResponseGrant signin = Helper.LookupSignIn(Options.AuthenticationType); + bool shouldSignin = signin != null; + AuthenticationResponseRevoke signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode); + bool shouldSignout = signout != null; + + if (shouldSignin || shouldSignout || _shouldRenew) + { + var cookieOptions = new CookieOptions + { + Domain = Options.CookieDomain, + HttpOnly = Options.CookieHttpOnly, + Path = Options.CookiePath ?? "/", + }; + if (Options.CookieSecure == CookieSecureOption.SameAsRequest) + { + cookieOptions.Secure = Request.IsSecure; + } + else + { + cookieOptions.Secure = Options.CookieSecure == CookieSecureOption.Always; + } + + if (shouldSignin) + { + var context = new CookieResponseSignInContext( + Context, + Options, + Options.AuthenticationType, + signin.Identity, + signin.Properties); + + DateTimeOffset issuedUtc = Options.SystemClock.UtcNow; + DateTimeOffset expiresUtc = issuedUtc.Add(Options.ExpireTimeSpan); + + context.Properties.IssuedUtc = issuedUtc; + context.Properties.ExpiresUtc = expiresUtc; + + Options.Provider.ResponseSignIn(context); + + if (context.Properties.IsPersistent) + { + cookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; + } + + var model = new AuthenticationTicket(context.Identity, context.Properties); + string cookieValue = Options.TicketDataFormat.Protect(model); + + Response.Cookies.Append( + Options.CookieName, + cookieValue, + cookieOptions); + } + else if (shouldSignout) + { + Response.Cookies.Delete( + Options.CookieName, + cookieOptions); + } + else if (_shouldRenew) + { + AuthenticationTicket model = await AuthenticateAsync(); + + model.Properties.IssuedUtc = _renewIssuedUtc; + model.Properties.ExpiresUtc = _renewExpiresUtc; + + string cookieValue = Options.TicketDataFormat.Protect(model); + + if (model.Properties.IsPersistent) + { + cookieOptions.Expires = _renewExpiresUtc.ToUniversalTime().DateTime; + } + + Response.Cookies.Append( + Options.CookieName, + cookieValue, + cookieOptions); + } + + //Response.Headers.Set( + // HeaderNameCacheControl, + // HeaderValueNoCache); + + //Response.Headers.Set( + // HeaderNamePragma, + // HeaderValueNoCache); + + //Response.Headers.Set( + // HeaderNameExpires, + // HeaderValueMinusOne); + + bool shouldLoginRedirect = shouldSignin && Options.LoginPath.HasValue && Request.Path == Options.LoginPath; + bool shouldLogoutRedirect = shouldSignout && Options.LogoutPath.HasValue && Request.Path == Options.LogoutPath; + + if ((shouldLoginRedirect || shouldLogoutRedirect) && Response.StatusCode == 200) + { + IReadableStringCollection query = Request.Query; + string redirectUri = query.Get(Options.ReturnUrlParameter); + if (!string.IsNullOrWhiteSpace(redirectUri) + //&& IsHostRelative(redirectUri) + ) + { + var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri); + Options.Provider.ApplyRedirect(redirectContext); + } + } + } } private bool ShouldAuthRequest() { var httpContext = Context.HttpContextFromOwinContext(); - + // do not process if client-side request if (httpContext.Request.Url.IsClientSideRequest()) return false; @@ -62,21 +212,20 @@ namespace Umbraco.Web.Security.Identity /// Returns the current FormsAuth ticket in the request /// /// - /// /// - private static FormsAuthenticationTicket GetAuthTicket(IOwinRequest request, string cookieName) + private AuthenticationTicket GetAuthTicket(IOwinRequest request) { if (request == null) throw new ArgumentNullException("request"); - var formsCookie = request.Cookies[cookieName]; - if (formsCookie == null) + var formsCookie = request.Cookies[Options.CookieName]; + if (string.IsNullOrWhiteSpace(formsCookie)) { return null; } //get the ticket try { - return FormsAuthentication.Decrypt(formsCookie); + return Options.TicketDataFormat.Unprotect(formsCookie); } catch (Exception) { diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs index 1275b72c08..ffdce0fc8d 100644 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs @@ -2,25 +2,30 @@ using Microsoft.Owin.Security.Infrastructure; using Owin; using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.Logging; namespace Umbraco.Web.Security.Identity { /// /// Used to enable the normal Umbraco back office authentication to operate /// - public class UmbracoBackOfficeAuthenticationMiddleware : AuthenticationMiddleware + public class UmbracoBackOfficeAuthenticationMiddleware : AuthenticationMiddleware { - private readonly ISecuritySection _securitySection; + private readonly ILogger _logger; - public UmbracoBackOfficeAuthenticationMiddleware(OwinMiddleware next, IAppBuilder app, UmbracoBackOfficeAuthenticationOptions options, ISecuritySection securitySection) + public UmbracoBackOfficeAuthenticationMiddleware( + OwinMiddleware next, + IAppBuilder app, + UmbracoBackOfficeCookieAuthenticationOptions options, + ILogger logger) : base(next, options) { - _securitySection = securitySection; + _logger = logger; } - protected override AuthenticationHandler CreateHandler() + protected override AuthenticationHandler CreateHandler() { - return new UmbracoBackOfficeAuthenticationHandler(_securitySection); + return new UmbracoBackOfficeAuthenticationHandler(_logger); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs deleted file mode 100644 index c7609a8e83..0000000000 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationOptions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.Owin.Security; - -namespace Umbraco.Web.Security.Identity -{ - /// - /// Umbraco auth options - really just ensures that it is operating in Active mode - /// - public sealed class UmbracoBackOfficeAuthenticationOptions : AuthenticationOptions - { - public UmbracoBackOfficeAuthenticationOptions() - : base("UmbracoBackOffice") - { - //Must be active, this needs to look at each request to determine if it should execute, - // if set to passive this will not be the case - AuthenticationMode = AuthenticationMode.Active; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs new file mode 100644 index 0000000000..e23f30b27f --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs @@ -0,0 +1,35 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.Owin; +using Microsoft.Owin.Security.Cookies; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Umbraco auth cookie options + /// + public sealed class UmbracoBackOfficeCookieAuthenticationOptions : CookieAuthenticationOptions + { + public UmbracoBackOfficeCookieAuthenticationOptions() + : this(UmbracoConfig.For.UmbracoSettings().Security, GlobalSettings.TimeOutInMinutes, GlobalSettings.UseSSL) + { + } + + public UmbracoBackOfficeCookieAuthenticationOptions(ISecuritySection securitySection, int loginTimeoutMinutes, bool forceSsl) + { + AuthenticationType = "UmbracoBackOffice"; + + TicketDataFormat = new FormsAuthenticationSecureDataFormat(loginTimeoutMinutes); + + CookieDomain = securitySection.AuthCookieDomain; + CookieName = securitySection.AuthCookieName; + CookieHttpOnly = true; + CookieSecure = forceSsl ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest; + CookiePath = "/"; + LoginPath = new PathString("/umbraco/login"); //TODO: ?? + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 0cad922d16..b6ec3680d8 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -91,7 +91,7 @@ namespace Umbraco.Web.Security /// Logs the user in /// /// - /// returns the number of seconds until their session times out + /// returns the Forms Auth ticket created which is used to log them in public virtual FormsAuthenticationTicket PerformLogin(IUser user) { var ticket = _httpContext.CreateUmbracoAuthTicket(new UserData(Guid.NewGuid().ToString("N")) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 45e0ce5fe2..858561e5e9 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -138,8 +138,9 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll - - ..\packages\Microsoft.Owin.2.1.0\lib\net45\Microsoft.Owin.dll + + False + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll @@ -540,10 +541,11 @@ + - + diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index f2d216f2fd..719f7135c4 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -180,26 +180,26 @@ namespace Umbraco.Web /// static void AuthenticateRequest(object sender, EventArgs e) { - var app = (HttpApplication)sender; - var http = new HttpContextWrapper(app.Context); + //var app = (HttpApplication)sender; + //var http = new HttpContextWrapper(app.Context); - // do not process if client-side request - if (http.Request.Url.IsClientSideRequest()) - return; + //// do not process if client-side request + //if (http.Request.Url.IsClientSideRequest()) + // return; - var req = new HttpRequestWrapper(app.Request); + //var req = new HttpRequestWrapper(app.Request); - if (ShouldAuthenticateRequest(req, UmbracoContext.Current.OriginalRequestUrl)) - { - //TODO: Here we should have an authentication mechanism, this mechanism should be smart in the way that the ASP.Net 5 pipeline works - // in which each registered handler will attempt to authenticate and if it fails it will just call Next() so the next handler - // executes. If it is successful, it doesn't call next and assigns the current user/principal. - // This might actually all be possible with ASP.Net Identity and how it is setup to work already, need to investigate. + //if (ShouldAuthenticateRequest(req, UmbracoContext.Current.OriginalRequestUrl)) + //{ + // //TODO: Here we should have an authentication mechanism, this mechanism should be smart in the way that the ASP.Net 5 pipeline works + // // in which each registered handler will attempt to authenticate and if it fails it will just call Next() so the next handler + // // executes. If it is successful, it doesn't call next and assigns the current user/principal. + // // This might actually all be possible with ASP.Net Identity and how it is setup to work already, need to investigate. - var ticket = http.GetUmbracoAuthTicket(); + // var ticket = http.GetUmbracoAuthTicket(); - http.AuthenticateCurrentRequest(ticket, ShouldIgnoreTicketRenew(UmbracoContext.Current.OriginalRequestUrl, http) == false); - } + // http.AuthenticateCurrentRequest(ticket, ShouldIgnoreTicketRenew(UmbracoContext.Current.OriginalRequestUrl, http) == false); + //} } diff --git a/src/Umbraco.Web/app.config b/src/Umbraco.Web/app.config index 957569042f..051211ebf7 100644 --- a/src/Umbraco.Web/app.config +++ b/src/Umbraco.Web/app.config @@ -51,6 +51,10 @@ + + + + \ No newline at end of file diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 3fade0702d..8506c1de5b 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -19,7 +19,7 @@ - + diff --git a/src/umbraco.MacroEngines/app.config b/src/umbraco.MacroEngines/app.config index 900c3903d5..a3f8757270 100644 --- a/src/umbraco.MacroEngines/app.config +++ b/src/umbraco.MacroEngines/app.config @@ -26,6 +26,10 @@ + + + + \ No newline at end of file diff --git a/src/umbraco.businesslogic/packages.config b/src/umbraco.businesslogic/packages.config index 59350953ba..8ae1655c0d 100644 --- a/src/umbraco.businesslogic/packages.config +++ b/src/umbraco.businesslogic/packages.config @@ -4,5 +4,7 @@ + + \ No newline at end of file diff --git a/src/umbraco.businesslogic/umbraco.businesslogic.csproj b/src/umbraco.businesslogic/umbraco.businesslogic.csproj index c4c675366f..7accd2ccb5 100644 --- a/src/umbraco.businesslogic/umbraco.businesslogic.csproj +++ b/src/umbraco.businesslogic/umbraco.businesslogic.csproj @@ -113,10 +113,16 @@ ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll + + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + + ..\packages\Owin.1.0\lib\net40\Owin.dll + diff --git a/src/umbraco.editorControls/app.config b/src/umbraco.editorControls/app.config index 734aeed7b8..743b7c93ca 100644 --- a/src/umbraco.editorControls/app.config +++ b/src/umbraco.editorControls/app.config @@ -31,6 +31,10 @@ + + + + \ No newline at end of file From 48317d7e619778625d08a87e8566dfa49516a286 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 6 Feb 2015 14:05:29 +1100 Subject: [PATCH 03/38] massively simplifies the cookie handling, we don't use our own and just use the defaults, the trick to not validating everything is to use the cookie path. This does mean that each clientside request will also be validated but there's no way to override this behavior in identity currently, the cookie handler is internal so unless we copy/paste all of it's code can't do much about that. --- .../Security/AuthenticationExtensions.cs | 5 +- .../Security/Identity/AppBuilderExtensions.cs | 55 ++-- .../UmbracoBackOfficeAuthenticationHandler.cs | 236 ------------------ ...bracoBackOfficeAuthenticationMiddleware.cs | 31 --- ...coBackOfficeCookieAuthenticationOptions.cs | 21 +- src/Umbraco.Web/Umbraco.Web.csproj | 2 - 6 files changed, 38 insertions(+), 312 deletions(-) delete mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs delete mode 100644 src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 49511697d7..008f24f492 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -216,7 +216,7 @@ namespace Umbraco.Core.Security GlobalSettings.TimeOutInMinutes, //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way 1440, - "/", + "/umbraco", UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain); } @@ -443,7 +443,8 @@ namespace Umbraco.Core.Security hash) { Expires = DateTime.Now.AddMinutes(minutesPersisted), - Domain = cookieDomain + Domain = cookieDomain, + Path = cookiePath }; if (GlobalSettings.UseSSL) diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index dc68a43152..52eadd98da 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.Web; +using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using Microsoft.Owin.Extensions; +using Microsoft.Owin.Security.Cookies; using Owin; using Umbraco.Core; using Umbraco.Core.Configuration; @@ -51,47 +53,28 @@ namespace Umbraco.Web.Security.Identity { if (app == null) throw new ArgumentNullException("app"); - app.Use(typeof (UmbracoBackOfficeAuthenticationMiddleware), - //ctor params - app, - new UmbracoBackOfficeCookieAuthenticationOptions( + + app.UseCookieAuthentication(new UmbracoBackOfficeCookieAuthenticationOptions( UmbracoConfig.For.UmbracoSettings().Security, GlobalSettings.TimeOutInMinutes, - GlobalSettings.UseSSL), - LoggerResolver.Current.Logger); + GlobalSettings.UseSSL, + GlobalSettings.Path) + { + //Provider = new CookieAuthenticationProvider + //{ + // // 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 = SecurityStampValidator + // .OnValidateIdentity, UmbracoApplicationUser, int>( + // TimeSpan.FromMinutes(30), + // (manager, user) => user.GenerateUserIdentityAsync(manager), + // identity => identity.GetUserId()) + //} + }); - app.UseStageMarker(PipelineStage.Authenticate); return app; } - //This is a fix for OWIN mem leak! - //http://stackoverflow.com/questions/24378856/memory-leak-in-owin-appbuilderextensions/24819543#24819543 - private class OwinContextDisposal : IDisposable - where T1 : IDisposable - where T2 : IDisposable - { - private readonly List _disposables = new List(); - private bool _disposed = false; - - public OwinContextDisposal(IOwinContext owinContext) - { - if (HttpContext.Current == null) return; - - _disposables.Add(owinContext.Get()); - _disposables.Add(owinContext.Get()); - - HttpContext.Current.DisposeOnPipelineCompleted(this); - } - - public void Dispose() - { - if (_disposed) return; - foreach (var disposable in _disposables) - { - disposable.Dispose(); - } - _disposed = true; - } - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs deleted file mode 100644 index 3393075c14..0000000000 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationHandler.cs +++ /dev/null @@ -1,236 +0,0 @@ -using System; -using System.Reflection; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; -using System.Web; -using System.Web.Security; -using Microsoft.Owin; -using Microsoft.Owin.Security; -using Microsoft.Owin.Security.Cookies; -using Microsoft.Owin.Security.Infrastructure; -using Newtonsoft.Json; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Security; -using Umbraco.Core; -using Umbraco.Core.Logging; - -namespace Umbraco.Web.Security.Identity -{ - /// - /// Used to allow normal Umbraco back office authentication to work - /// - public class UmbracoBackOfficeAuthenticationHandler : AuthenticationHandler - { - private readonly ILogger _logger; - private bool _shouldRenew; - private DateTimeOffset _renewIssuedUtc; - private DateTimeOffset _renewExpiresUtc; - - public UmbracoBackOfficeAuthenticationHandler(ILogger logger) - { - _logger = logger; - } - - /// - /// Checks if we should authentication the request (i.e. is back office) and if so gets the forms auth ticket in the request - /// and returns an AuthenticationTicket based on that. - /// - /// - /// - /// It's worth noting that the UmbracoModule still executes and performs the authentication, however this also needs to execute - /// so that it assigns the new Principal object on the OWIN request: - /// http://brockallen.com/2013/10/27/host-authentication-and-web-api-with-owin-and-active-vs-passive-authentication-middleware/ - /// - protected override async Task AuthenticateCoreAsync() - { - if (ShouldAuthRequest()) - { - var ticket = GetAuthTicket(Request); - - if (ticket == null) - { - _logger.Warn(@"Unprotect ticket failed"); - return null; - } - - DateTimeOffset currentUtc = Options.SystemClock.UtcNow; - DateTimeOffset? issuedUtc = ticket.Properties.IssuedUtc; - DateTimeOffset? expiresUtc = ticket.Properties.ExpiresUtc; - - if (expiresUtc != null && expiresUtc.Value < currentUtc) - { - return null; - } - - if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration) - { - TimeSpan timeElapsed = currentUtc.Subtract(issuedUtc.Value); - TimeSpan timeRemaining = expiresUtc.Value.Subtract(currentUtc); - - if (timeRemaining < timeElapsed) - { - _shouldRenew = true; - _renewIssuedUtc = currentUtc; - TimeSpan timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value); - _renewExpiresUtc = currentUtc.Add(timeSpan); - } - } - - var context = new CookieValidateIdentityContext(Context, ticket, Options); - - await Options.Provider.ValidateIdentity(context); - - return new AuthenticationTicket(context.Identity, context.Properties); - } - - return await Task.FromResult(null); - } - - protected override async Task ApplyResponseGrantAsync() - { - AuthenticationResponseGrant signin = Helper.LookupSignIn(Options.AuthenticationType); - bool shouldSignin = signin != null; - AuthenticationResponseRevoke signout = Helper.LookupSignOut(Options.AuthenticationType, Options.AuthenticationMode); - bool shouldSignout = signout != null; - - if (shouldSignin || shouldSignout || _shouldRenew) - { - var cookieOptions = new CookieOptions - { - Domain = Options.CookieDomain, - HttpOnly = Options.CookieHttpOnly, - Path = Options.CookiePath ?? "/", - }; - if (Options.CookieSecure == CookieSecureOption.SameAsRequest) - { - cookieOptions.Secure = Request.IsSecure; - } - else - { - cookieOptions.Secure = Options.CookieSecure == CookieSecureOption.Always; - } - - if (shouldSignin) - { - var context = new CookieResponseSignInContext( - Context, - Options, - Options.AuthenticationType, - signin.Identity, - signin.Properties); - - DateTimeOffset issuedUtc = Options.SystemClock.UtcNow; - DateTimeOffset expiresUtc = issuedUtc.Add(Options.ExpireTimeSpan); - - context.Properties.IssuedUtc = issuedUtc; - context.Properties.ExpiresUtc = expiresUtc; - - Options.Provider.ResponseSignIn(context); - - if (context.Properties.IsPersistent) - { - cookieOptions.Expires = expiresUtc.ToUniversalTime().DateTime; - } - - var model = new AuthenticationTicket(context.Identity, context.Properties); - string cookieValue = Options.TicketDataFormat.Protect(model); - - Response.Cookies.Append( - Options.CookieName, - cookieValue, - cookieOptions); - } - else if (shouldSignout) - { - Response.Cookies.Delete( - Options.CookieName, - cookieOptions); - } - else if (_shouldRenew) - { - AuthenticationTicket model = await AuthenticateAsync(); - - model.Properties.IssuedUtc = _renewIssuedUtc; - model.Properties.ExpiresUtc = _renewExpiresUtc; - - string cookieValue = Options.TicketDataFormat.Protect(model); - - if (model.Properties.IsPersistent) - { - cookieOptions.Expires = _renewExpiresUtc.ToUniversalTime().DateTime; - } - - Response.Cookies.Append( - Options.CookieName, - cookieValue, - cookieOptions); - } - - //Response.Headers.Set( - // HeaderNameCacheControl, - // HeaderValueNoCache); - - //Response.Headers.Set( - // HeaderNamePragma, - // HeaderValueNoCache); - - //Response.Headers.Set( - // HeaderNameExpires, - // HeaderValueMinusOne); - - bool shouldLoginRedirect = shouldSignin && Options.LoginPath.HasValue && Request.Path == Options.LoginPath; - bool shouldLogoutRedirect = shouldSignout && Options.LogoutPath.HasValue && Request.Path == Options.LogoutPath; - - if ((shouldLoginRedirect || shouldLogoutRedirect) && Response.StatusCode == 200) - { - IReadableStringCollection query = Request.Query; - string redirectUri = query.Get(Options.ReturnUrlParameter); - if (!string.IsNullOrWhiteSpace(redirectUri) - //&& IsHostRelative(redirectUri) - ) - { - var redirectContext = new CookieApplyRedirectContext(Context, Options, redirectUri); - Options.Provider.ApplyRedirect(redirectContext); - } - } - } - } - - private bool ShouldAuthRequest() - { - var httpContext = Context.HttpContextFromOwinContext(); - - // do not process if client-side request - if (httpContext.Request.Url.IsClientSideRequest()) - return false; - - return UmbracoModule.ShouldAuthenticateRequest(httpContext.Request, Request.Uri); - } - - /// - /// Returns the current FormsAuth ticket in the request - /// - /// - /// - private AuthenticationTicket GetAuthTicket(IOwinRequest request) - { - if (request == null) throw new ArgumentNullException("request"); - - var formsCookie = request.Cookies[Options.CookieName]; - if (string.IsNullOrWhiteSpace(formsCookie)) - { - return null; - } - //get the ticket - try - { - return Options.TicketDataFormat.Unprotect(formsCookie); - } - catch (Exception) - { - return null; - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs deleted file mode 100644 index ffdce0fc8d..0000000000 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeAuthenticationMiddleware.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.Owin; -using Microsoft.Owin.Security.Infrastructure; -using Owin; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Logging; - -namespace Umbraco.Web.Security.Identity -{ - /// - /// Used to enable the normal Umbraco back office authentication to operate - /// - public class UmbracoBackOfficeAuthenticationMiddleware : AuthenticationMiddleware - { - private readonly ILogger _logger; - - public UmbracoBackOfficeAuthenticationMiddleware( - OwinMiddleware next, - IAppBuilder app, - UmbracoBackOfficeCookieAuthenticationOptions options, - ILogger logger) - : base(next, options) - { - _logger = logger; - } - - protected override AuthenticationHandler CreateHandler() - { - return new UmbracoBackOfficeAuthenticationHandler(_logger); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs index e23f30b27f..ac77a49db3 100644 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Microsoft.Owin; using Microsoft.Owin.Security.Cookies; +using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; @@ -13,22 +14,32 @@ namespace Umbraco.Web.Security.Identity public sealed class UmbracoBackOfficeCookieAuthenticationOptions : CookieAuthenticationOptions { public UmbracoBackOfficeCookieAuthenticationOptions() - : this(UmbracoConfig.For.UmbracoSettings().Security, GlobalSettings.TimeOutInMinutes, GlobalSettings.UseSSL) + : this(UmbracoConfig.For.UmbracoSettings().Security, GlobalSettings.TimeOutInMinutes, GlobalSettings.UseSSL, GlobalSettings.Path) { } - public UmbracoBackOfficeCookieAuthenticationOptions(ISecuritySection securitySection, int loginTimeoutMinutes, bool forceSsl) + public UmbracoBackOfficeCookieAuthenticationOptions( + ISecuritySection securitySection, + int loginTimeoutMinutes, + bool forceSsl, + string umbracoPath, + bool useLegacyFormsAuthDataFormat = true) { AuthenticationType = "UmbracoBackOffice"; - TicketDataFormat = new FormsAuthenticationSecureDataFormat(loginTimeoutMinutes); + if (useLegacyFormsAuthDataFormat) + { + //If this is not explicitly set it will fall back to the default automatically + TicketDataFormat = new FormsAuthenticationSecureDataFormat(loginTimeoutMinutes); + } CookieDomain = securitySection.AuthCookieDomain; CookieName = securitySection.AuthCookieName; CookieHttpOnly = true; CookieSecure = forceSsl ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest; - CookiePath = "/"; - LoginPath = new PathString("/umbraco/login"); //TODO: ?? + + //Ensure the cookie path is set so that it isn't transmitted for anything apart from requests to the back office + CookiePath = umbracoPath.EnsureStartsWith('/'); } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 858561e5e9..5be8656b13 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -543,8 +543,6 @@ - - From 927add6f4446afaba9d86397c1054d80175d23a2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 6 Feb 2015 16:13:02 +1100 Subject: [PATCH 04/38] Updates UmbracoBackOfficeIdentity to add claims and adds a new ctor so people can create an identity manually - this is really the key, by doing this we'd already be able to have 3rd party authentication happening. Ensures our custom secure data format persists the user data --- src/Umbraco.Core/Constants-Web.cs | 12 ++ .../Security/UmbracoBackOfficeIdentity.cs | 138 ++++++++++-------- src/Umbraco.Core/Umbraco.Core.csproj | 22 +++ src/Umbraco.Core/packages.config | 5 + src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 6 +- .../Security/Identity/AppBuilderExtensions.cs | 35 +++-- .../FormsAuthenticationSecureDataFormat.cs | 17 ++- ...coBackOfficeCookieAuthenticationOptions.cs | 6 +- src/umbraco.datalayer/app.config | 4 + src/umbraco.providers/app.config | 4 + 10 files changed, 168 insertions(+), 81 deletions(-) diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 83cb995eeb..f6c2df14c3 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -18,5 +18,17 @@ public const string AuthCookieName = "UMB_UCONTEXT"; } + + public static class Security + { + + public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; + public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; + public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapps"; + public const string UserIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/userid"; + public const string CultureClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/culture"; + public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; + + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index 2737c636a3..be6811ad7b 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -1,115 +1,137 @@ using System; using System.Runtime.Serialization; +using System.Security.Claims; +using System.Security.Principal; using System.Web; using System.Web.Security; using Newtonsoft.Json; namespace Umbraco.Core.Security { + /// /// A custom user identity for the Umbraco backoffice /// /// - /// All values are lazy loaded for performance reasons as the constructor is called for every single request + /// This inherits from FormsIdentity for backwards compatibility reasons since we still support the forms auth cookie, in v8 we can + /// change over to 'pure' asp.net identity and just inherit from ClaimsIdentity. /// [Serializable] public class UmbracoBackOfficeIdentity : FormsIdentity { - public UmbracoBackOfficeIdentity(FormsAuthenticationTicket ticket) + /// + /// Create a back office identity based on user data + /// + /// + public UmbracoBackOfficeIdentity(UserData userdata) + //This just creates a temp/fake ticket + : base(new FormsAuthenticationTicket(userdata.Username, true, 10)) + { + UserData = userdata; + AddClaims(); + } + + /// + /// Create a new identity from a forms auth ticket + /// + /// + public UmbracoBackOfficeIdentity(FormsAuthenticationTicket ticket) : base(ticket) { - UserData = ticket.UserData; - EnsureDeserialized(); + UserData = JsonConvert.DeserializeObject(ticket.UserData); + AddClaims(); + } + + /// + /// Used for cloning + /// + /// + private UmbracoBackOfficeIdentity(UmbracoBackOfficeIdentity identity) + : base(identity) + { + UserData = identity.UserData; + AddClaims(); + } + + public static string Issuer = "UmbracoBackOffice"; + + //TODO: Another option is to create a ClaimsIdentityFactory when everything is wired up... optional though i think + private void AddClaims() + { + AddClaims(new[] + { + new Claim(Constants.Security.StartContentNodeIdClaimType, StartContentNode.ToInvariantString(), null, Issuer, Issuer, this), + new Claim(Constants.Security.StartMediaNodeIdClaimType, StartMediaNode.ToInvariantString(), null, Issuer, Issuer, this), + new Claim(Constants.Security.AllowedApplicationsClaimType, string.Join(",", AllowedApplications), null, Issuer, Issuer, this), + new Claim(Constants.Security.UserIdClaimType, Id.ToString(), null, Issuer, Issuer, this), + new Claim(Constants.Security.CultureClaimType, Culture, null, Issuer, Issuer, this), + new Claim(Constants.Security.SessionIdClaimType, SessionId, null, Issuer, Issuer, this), + new Claim(ClaimTypes.Role, string.Join(",", Roles), null, Issuer, Issuer, this) + }); + } + + protected internal UserData UserData { get; private set; } + + /// + /// Gets the type of authenticated identity. + /// + /// + /// The type of authenticated identity. This property always returns "UmbracoBackOffice". + /// + public override string AuthenticationType + { + get { return Issuer; } } - - protected readonly string UserData; - internal UserData DeserializedData; public int StartContentNode { - get - { - return DeserializedData.StartContentNode; - } + get { return UserData.StartContentNode; } } public int StartMediaNode { - get { return DeserializedData.StartMediaNode; } + get { return UserData.StartMediaNode; } } public string[] AllowedApplications { - get { return DeserializedData.AllowedApplications; } + get { return UserData.AllowedApplications; } } - + public object Id { - get { return DeserializedData.Id; } + get { return UserData.Id; } } public string RealName { - get { return DeserializedData.RealName; } + get { return UserData.RealName; } } public string Culture { - get { return DeserializedData.Culture; } + get { return UserData.Culture; } } public string SessionId { - get { return DeserializedData.SessionId; } + get { return UserData.SessionId; } } - //public int SessionTimeout - //{ - // get - // { - // EnsureDeserialized(); - // return DeserializedData.SessionTimeout; - // } - //} - public string[] Roles { - get { return DeserializedData.Roles; } + get { return UserData.Roles; } } /// - /// This will ensure we only deserialize once + /// Gets a copy of the current instance. /// - /// - /// For performance reasons, we'll also check if there's an http context available, - /// if so, we'll chuck our instance in there so that we only deserialize once per request. - /// - protected void EnsureDeserialized() + /// + /// A copy of the current instance. + /// + public override ClaimsIdentity Clone() { - if (DeserializedData != null) - return; - - if (HttpContext.Current != null) - { - //check if we've already done this in this request - var data = HttpContext.Current.Items[typeof(UmbracoBackOfficeIdentity)] as UserData; - if (data != null) - { - DeserializedData = data; - return; - } - } - - if (string.IsNullOrEmpty(UserData)) - { - throw new NullReferenceException("The " + typeof(UserData) + " found in the ticket cannot be empty"); - } - DeserializedData = JsonConvert.DeserializeObject(UserData); - - if (HttpContext.Current != null) - { - HttpContext.Current.Items[typeof (UmbracoBackOfficeIdentity)] = DeserializedData; - } + return new UmbracoBackOfficeIdentity(this); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 9579bbba59..c5da56af3d 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -54,9 +54,25 @@ False ..\packages\log4net-mediumtrust.2.0.0\lib\log4net.dll + + ..\packages\Microsoft.AspNet.Identity.Core.2.1.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + + + ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + + ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll + + + ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + + + ..\packages\Microsoft.Owin.Security.OAuth.2.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + True + True ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll @@ -326,6 +342,10 @@ + + + + @@ -343,6 +363,7 @@ + @@ -387,6 +408,7 @@ + diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index c13e24ef1a..559eae83c4 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -3,6 +3,8 @@ + + @@ -11,6 +13,9 @@ + + + diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index e215fd552c..ba6e3b3d59 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -50,9 +50,11 @@ namespace Umbraco.Web.UI //Ensure owin is configured for Umbraco back office authentication - this must // be configured AFTER the standard UseCookieConfiguration above. - app.UseUmbracoBackAuthentication(); + app + .UseUmbracoBackOfficeCookieAuthentication() + .UseUmbracoBackOfficeExternalCookieAuthentication(); - app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); + //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); } diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 52eadd98da..d408e7a98a 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; using System.Web; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; @@ -49,7 +51,7 @@ namespace Umbraco.Web.Security.Identity /// /// /// - public static IAppBuilder UseUmbracoBackAuthentication(this IAppBuilder app) + public static IAppBuilder UseUmbracoBackOfficeCookieAuthentication(this IAppBuilder app) { if (app == null) throw new ArgumentNullException("app"); @@ -60,21 +62,30 @@ namespace Umbraco.Web.Security.Identity GlobalSettings.UseSSL, GlobalSettings.Path) { - //Provider = new CookieAuthenticationProvider - //{ - // // 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 = SecurityStampValidator - // .OnValidateIdentity, UmbracoApplicationUser, int>( - // TimeSpan.FromMinutes(30), - // (manager, user) => user.GenerateUserIdentityAsync(manager), - // identity => identity.GetUserId()) - //} + Provider = new CookieAuthenticationProvider + { + //// 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 = SecurityStampValidator + // .OnValidateIdentity, UmbracoApplicationUser, int>( + // TimeSpan.FromMinutes(30), + // (manager, user) => user.GenerateUserIdentityAsync(manager), + // identity => identity.GetUserId()) + } }); return app; } + public static IAppBuilder UseUmbracoBackOfficeExternalCookieAuthentication(this IAppBuilder app) + { + if (app == null) throw new ArgumentNullException("app"); + + app.UseExternalSignInCookie("UmbracoExternalCookie"); + + return app; + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs b/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs index fac130f0ca..c2c4810564 100644 --- a/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs +++ b/src/Umbraco.Web/Security/Identity/FormsAuthenticationSecureDataFormat.cs @@ -1,4 +1,5 @@ using System; +using System.Security.Claims; using System.Web.Security; using Microsoft.Owin.Security; using Newtonsoft.Json; @@ -12,16 +13,18 @@ namespace Umbraco.Web.Security.Identity internal class FormsAuthenticationSecureDataFormat : ISecureDataFormat { private readonly int _loginTimeoutMinutes; + private readonly string _cookiePath; - public FormsAuthenticationSecureDataFormat(int loginTimeoutMinutes) + public FormsAuthenticationSecureDataFormat(int loginTimeoutMinutes, string cookiePath) { _loginTimeoutMinutes = loginTimeoutMinutes; + _cookiePath = cookiePath; } public string Protect(AuthenticationTicket data) { - //TODO: Where to get the user data? - //var userDataString = JsonConvert.SerializeObject(userdata); + var backofficeIdentity = (UmbracoBackOfficeIdentity)data.Identity; + var userDataString = JsonConvert.SerializeObject(backofficeIdentity.UserData); var ticket = new FormsAuthenticationTicket( 5, @@ -29,8 +32,8 @@ namespace Umbraco.Web.Security.Identity data.Properties.IssuedUtc.HasValue ? data.Properties.IssuedUtc.Value.LocalDateTime : DateTime.Now, data.Properties.ExpiresUtc.HasValue ? data.Properties.ExpiresUtc.Value.LocalDateTime : DateTime.Now.AddMinutes(_loginTimeoutMinutes), data.Properties.IsPersistent, - "", //User data here!! This will come from the identity - "/" + userDataString, + _cookiePath ); return FormsAuthentication.Encrypt(ticket); @@ -51,12 +54,14 @@ namespace Umbraco.Web.Security.Identity var identity = new UmbracoBackOfficeIdentity(decrypt); - return new AuthenticationTicket(identity, new AuthenticationProperties + var ticket = new AuthenticationTicket(identity, new AuthenticationProperties { ExpiresUtc = decrypt.Expiration.ToUniversalTime(), IssuedUtc = decrypt.IssueDate.ToUniversalTime(), IsPersistent = decrypt.IsPersistent }); + + return ticket; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs index ac77a49db3..41d52e0684 100644 --- a/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs +++ b/src/Umbraco.Web/Security/Identity/UmbracoBackOfficeCookieAuthenticationOptions.cs @@ -22,7 +22,7 @@ namespace Umbraco.Web.Security.Identity ISecuritySection securitySection, int loginTimeoutMinutes, bool forceSsl, - string umbracoPath, + string cookiePath, bool useLegacyFormsAuthDataFormat = true) { AuthenticationType = "UmbracoBackOffice"; @@ -30,7 +30,7 @@ namespace Umbraco.Web.Security.Identity if (useLegacyFormsAuthDataFormat) { //If this is not explicitly set it will fall back to the default automatically - TicketDataFormat = new FormsAuthenticationSecureDataFormat(loginTimeoutMinutes); + TicketDataFormat = new FormsAuthenticationSecureDataFormat(loginTimeoutMinutes, cookiePath); } CookieDomain = securitySection.AuthCookieDomain; @@ -39,7 +39,7 @@ namespace Umbraco.Web.Security.Identity CookieSecure = forceSsl ? CookieSecureOption.Always : CookieSecureOption.SameAsRequest; //Ensure the cookie path is set so that it isn't transmitted for anything apart from requests to the back office - CookiePath = umbracoPath.EnsureStartsWith('/'); + CookiePath = cookiePath.EnsureStartsWith('/'); } } diff --git a/src/umbraco.datalayer/app.config b/src/umbraco.datalayer/app.config index 8f828418f3..6acdd4b4ca 100644 --- a/src/umbraco.datalayer/app.config +++ b/src/umbraco.datalayer/app.config @@ -14,6 +14,10 @@ + + + + \ No newline at end of file diff --git a/src/umbraco.providers/app.config b/src/umbraco.providers/app.config index b77bae14a4..267e89a2dd 100644 --- a/src/umbraco.providers/app.config +++ b/src/umbraco.providers/app.config @@ -14,6 +14,10 @@ + + + + \ No newline at end of file From d4b21243ca9aaf0940e8511a2220cb49a4f795e3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 9 Feb 2015 12:14:59 +1100 Subject: [PATCH 05/38] Allows external logins to be listed on login page, updates BackOfficeController with actions for invoking them. --- .../Security/AuthenticationExtensions.cs | 6 +- src/Umbraco.Core/Umbraco.Core.csproj | 5 +- src/Umbraco.Core/packages.config | 2 +- .../views/common/dialogs/login.controller.js | 6 +- .../src/views/common/dialogs/login.html | 42 +++++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 10 +- src/Umbraco.Web.UI/packages.config | 3 +- .../umbraco/Views/Default.cshtml | 31 +++-- .../Editors/BackOfficeController.cs | 107 ++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 11 +- src/Umbraco.Web/packages.config | 3 +- 11 files changed, 191 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 008f24f492..dd688cfe25 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -323,12 +323,12 @@ namespace Umbraco.Core.Security private static FormsAuthenticationTicket GetAuthTicket(this HttpContextBase http, string cookieName) { - var allKeys = new List(); + var asDictionary = new Dictionary(); for (var i = 0; i < http.Request.Cookies.Keys.Count; i++) { - allKeys.Add(http.Request.Cookies.Keys.Get(i)); + var key = http.Request.Cookies.Keys.Get(i); + asDictionary[key] = http.Request.Cookies[key].Value; } - var asDictionary = allKeys.ToDictionary(key => key, key => http.Request.Cookies[key].Value); //get the ticket try diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c5da56af3d..f0f09c499c 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -63,8 +63,9 @@ ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll - - ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll + + False + ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index 559eae83c4..93c9737158 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -13,7 +13,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index c9e7adabe2..00d3cda1ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -16,7 +16,11 @@ }); // weekday[d.getDay()]; $scope.errorMsg = ""; - + + $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; + + $scope.externalLogins = Umbraco.Sys.ServerVariables.externalLogins; + $scope.loginSubmit = function (login, password) { //if the login and password are not empty we need to automatically diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 865dd8303f..5b73743772 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -1,4 +1,4 @@ -
+
@@ -8,19 +8,37 @@ Log in below

-
- -
+ +
+ +
-
- -
+
+ +
- + + +
+
{{errorMsg}}
+
+ + +

+ +
+ +
+ + + +
+
+ -
-
{{errorMsg}}
-
- \ No newline at end of file +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index fb07ae1ff4..0cec068f48 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -162,15 +162,20 @@ False ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll
- + + False ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll + ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll + True ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + + ..\packages\Microsoft.Owin.Security.Google.3.0.0\lib\net45\Microsoft.Owin.Security.Google.dll + ..\packages\Microsoft.Owin.Security.OAuth.2.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll @@ -202,6 +207,7 @@ ..\packages\Owin.1.0\lib\net40\Owin.dll + True System diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 150759f582..5ed2c4b1d0 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -25,8 +25,9 @@ - + + diff --git a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml index 308f483af2..8369b5b677 100644 --- a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml @@ -3,6 +3,9 @@ @using Umbraco.Core @using ClientDependency.Core @using ClientDependency.Core.Mvc +@using Microsoft.Owin.Security +@using Newtonsoft.Json +@using Newtonsoft.Json.Linq @using Umbraco.Core.IO @using Umbraco.Web @using Umbraco.Web.Editors @@ -23,17 +26,17 @@ Umbraco - + @{ Html.RequiresCss("assets/css/umbraco.css", "Umbraco");} @{ Html.RequiresCss("tree/treeicons.css", "UmbracoClient");} @Html.RenderCssHere( new BasicPath("Umbraco", IOHelper.ResolveUrl(SystemDirectories.Umbraco)), new BasicPath("UmbracoClient", IOHelper.ResolveUrl(SystemDirectories.UmbracoClient))) - + -
+
@@ -47,18 +50,28 @@ + @{ + var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes() + .Select(p => new {authType = p.AuthenticationType, caption = p.Caption, + //TODO: Need to see if this exposes any sensitive data! + properties = p.Properties}) + .ToArray(); + } + @* These are the bare minimal server variables that are required for the application to start without being authenticated, we will load the rest of the server vars after the user is authenticated. - *@ + *@ - + @{ var isDebug = false; if (Request.RawUrl.IndexOf('?') >= 0) @@ -78,9 +91,9 @@ if (attempt && attempt.Result) { isDebug = true; - } + } } - + } @if (isDebug) { diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 46028bff35..e844d8f5fb 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -5,9 +5,13 @@ using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; +using System.Threading.Tasks; using System.Web.Mvc; using System.Web.UI; using dotless.Core.Parser.Tree; +using Microsoft.AspNet.Identity; +using Microsoft.Owin; +using Microsoft.Owin.Security; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core.Configuration; @@ -27,6 +31,9 @@ using Umbraco.Web.PropertyEditors; using Umbraco.Web.Models; using Umbraco.Web.WebServices; using Umbraco.Web.WebApi.Filters; +using System.Web; +using AutoMapper; +using Umbraco.Core.Security; namespace Umbraco.Web.Editors { @@ -37,12 +44,18 @@ namespace Umbraco.Web.Editors [DisableClientCache] public class BackOfficeController : UmbracoController { + protected IOwinContext OwinContext + { + get { return Request.GetOwinContext(); } + } + /// /// Render the default view /// /// public ActionResult Default() { + ViewBag.UmbracoPath = GlobalSettings.UmbracoMvcArea; return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); } @@ -359,6 +372,62 @@ namespace Umbraco.Web.Editors return JavaScript(ServerVariablesParser.Parse(result)); } + [HttpPost] + [AllowAnonymous] + public ActionResult ExternalLogin(string provider, string returnUrl = null) + { + if (returnUrl.IsNullOrWhiteSpace()) + { + returnUrl = GlobalSettings.Path; + } + + // Request a redirect to the external login provider + return new ChallengeResult(provider, + Url.Action("ExternalLoginCallback", "BackOffice", new + { + area = GlobalSettings.UmbracoMvcArea, + ReturnUrl = returnUrl + })); + } + + [HttpGet] + [AllowAnonymous] + public async Task ExternalLoginCallback(string returnUrl) + { + var loginInfo = await OwinContext.Authentication.GetExternalLoginInfoAsync(); + if (loginInfo == null) + { + //go home, invalid callback + return RedirectToLocal(returnUrl); + } + + //// 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) + //{ + // await SignInAsync(user, isPersistent: false); + // return RedirectToLocal(returnUrl); + //} + //else + //{ + // // If the user does not have an account, then prompt the user to create an account + // ViewBag.ReturnUrl = returnUrl; + // ViewBag.LoginProvider = loginInfo.Login.LoginProvider; + + // return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email }); + //} + + //TODO: until we make a user thingy and make this correctly , we'll just see if this works + + var user = Security.GetBackOfficeUser(loginInfo.DefaultUserName); + + var ticket = UmbracoContext.Security.PerformLogin(user); + HttpContext.AuthenticateCurrentRequest(ticket, false); + + return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); + } + + /// /// Returns the server variables regarding the application state /// @@ -505,5 +574,43 @@ namespace Umbraco.Web.Editors JsUrl } + private ActionResult RedirectToLocal(string returnUrl) + { + if (Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); + } + return Redirect("/"); + } + + // Used for XSRF protection when adding external logins + private const string XsrfKey = "XsrfId"; + + private class ChallengeResult : HttpUnauthorizedResult + { + public ChallengeResult(string provider, string redirectUri, string userId = null) + { + LoginProvider = provider; + RedirectUri = redirectUri; + UserId = userId; + } + + private string LoginProvider { get; set; } + private string RedirectUri { get; set; } + private string UserId { get; set; } + + public override void ExecuteResult(ControllerContext context) + { + //Ensure the forms auth module doesn't do a redirect! + context.HttpContext.Response.SuppressFormsAuthenticationRedirect = true; + + var properties = new AuthenticationProperties() { RedirectUri = RedirectUri }; + if (UserId != null) + { + properties.Dictionary[XsrfKey] = UserId; + } + context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider); + } + } } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5be8656b13..8520557feb 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -142,8 +142,12 @@ False ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll - - ..\packages\Microsoft.Owin.Security.2.1.0\lib\net45\Microsoft.Owin.Security.dll + + ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + + False + ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll @@ -167,7 +171,8 @@ False ..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll - + + False ..\packages\Owin.1.0\lib\net40\Owin.dll diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 8506c1de5b..877034b4e5 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -20,7 +20,8 @@ - + + From 8c51e8bad8a428056b6009386ea9b7349561f471 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 9 Feb 2015 17:37:21 +1100 Subject: [PATCH 06/38] Implements IExternalLoginService and the db table, implements more logic to start enabling this to work in the back office, need to implement the rest of the userstore and then implement a way once logged in to the back office to allow users to link their accounts with external logins. Currently if an external login is detected during startup and it has not been linked we'll throw an exception. Very very close to making this all work nicely. --- src/Umbraco.Core/Constants-Web.cs | 2 + .../Models/Identity/BackOfficeIdentityUser.cs | 60 +++++ .../Models/Identity/IIdentityUserLogin.cs | 25 ++ .../Models/Identity/IdentityUser.cs | 122 +++++++++ .../Models/Identity/IdentityUserClaim.cs | 38 +++ .../Models/Identity/IdentityUserLogin.cs | 39 +++ .../Models/Identity/IdentityUserRole.cs | 26 ++ .../Models/Rdbms/ExternalLoginDto.cs | 35 +++ .../Factories/ExternalLoginFactory.cs | 32 +++ .../Initial/DatabaseSchemaCreation.cs | 4 +- .../AddExternalLoginsTable.cs | 28 ++ .../Repositories/ExternalLoginRepository.cs | 182 +++++++++++++ .../Interfaces/IExternalLoginRepository.cs | 12 + .../Repositories/UserRepository.cs | 3 +- .../Persistence/RepositoryFactory.cs | 7 + .../Security/AuthenticationExtensions.cs | 6 +- .../BackOfficeClaimsIdentityFactory.cs | 35 +++ .../Security/BackOfficeUserManager.cs | 157 +++++++++++ .../Security/BackOfficeUserStore.cs | 251 ++++++++++++++++++ .../Security/MembershipProviderBase.cs | 13 + .../Security/MembershipProviderExtensions.cs | 12 +- .../Security/UmbracoBackOfficeIdentity.cs | 61 ++++- .../Security/UmbracoMembershipProviderBase.cs | 2 + .../Services/ExternalLoginService.cs | 78 ++++++ .../Services/IExternalLoginService.cs | 40 +++ src/Umbraco.Core/Services/IFileService.cs | 1 + src/Umbraco.Core/Services/ServiceContext.cs | 12 +- src/Umbraco.Core/Umbraco.Core.csproj | 21 +- src/Umbraco.Core/packages.config | 4 +- .../views/common/dialogs/login.controller.js | 5 +- .../src/views/common/dialogs/login.html | 4 +- src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 15 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 10 +- src/Umbraco.Web.UI/packages.config | 4 +- .../umbraco/Views/Default.cshtml | 28 +- src/Umbraco.Web/BaseRest/BaseRestHandler.cs | 1 + .../Editors/BackOfficeController.cs | 98 ++++--- src/Umbraco.Web/IUmbracoContextAccessor.cs | 13 + .../Security/Identity/AppBuilderExtensions.cs | 57 ++-- .../Identity/BackOfficeCookieManager.cs | 45 ++++ .../FormsAuthenticationSecureDataFormat.cs | 15 +- ...coBackOfficeCookieAuthenticationOptions.cs | 13 +- .../SingletonUmbracoContextAccessor.cs | 10 + src/Umbraco.Web/Umbraco.Web.csproj | 13 +- src/Umbraco.Web/UmbracoContext.cs | 1 - src/Umbraco.Web/packages.config | 4 +- 46 files changed, 1497 insertions(+), 147 deletions(-) create mode 100644 src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs create mode 100644 src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs create mode 100644 src/Umbraco.Core/Models/Identity/IdentityUser.cs create mode 100644 src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs create mode 100644 src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs create mode 100644 src/Umbraco.Core/Models/Identity/IdentityUserRole.cs create mode 100644 src/Umbraco.Core/Models/Rdbms/ExternalLoginDto.cs create mode 100644 src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs create mode 100644 src/Umbraco.Core/Persistence/Repositories/Interfaces/IExternalLoginRepository.cs create mode 100644 src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs create mode 100644 src/Umbraco.Core/Security/BackOfficeUserManager.cs create mode 100644 src/Umbraco.Core/Security/BackOfficeUserStore.cs create mode 100644 src/Umbraco.Core/Services/ExternalLoginService.cs create mode 100644 src/Umbraco.Core/Services/IExternalLoginService.cs create mode 100644 src/Umbraco.Web/IUmbracoContextAccessor.cs create mode 100644 src/Umbraco.Web/Security/Identity/BackOfficeCookieManager.cs create mode 100644 src/Umbraco.Web/SingletonUmbracoContextAccessor.cs diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index f6c2df14c3..e2f2841dc3 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -22,6 +22,8 @@ public static class Security { + public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; + public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapps"; diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs new file mode 100644 index 0000000000..087f5913e6 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; + +namespace Umbraco.Core.Models.Identity +{ + public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim> + { + /// + /// Gets/sets the user's real name + /// + public string Name { get; set; } + public int StartContentNode { get; set; } + public int StartMediaNode { get; set; } + public string[] AllowedApplications { get; set; } + public string Culture { get; set; } + + /// + /// Overridden to make the retrieval lazy + /// + public override ICollection Logins + { + get + { + if (_getLogins != null && _getLogins.IsValueCreated == false) + { + _logins = new ObservableCollection(); + foreach (var l in _getLogins.Value) + { + _logins.Add(l); + } + //now assign events + _logins.CollectionChanged += Logins_CollectionChanged; + } + return _logins; + } + } + + public bool LoginsChanged { get; private set; } + + void Logins_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + LoginsChanged = true; + } + + private ObservableCollection _logins; + private Lazy> _getLogins; + + /// + /// Used to set a lazy call back to populate the user's Login list + /// + /// + public void SetLoginsCallback(Lazy> callback) + { + if (callback == null) throw new ArgumentNullException("callback"); + _getLogins = callback; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs new file mode 100644 index 0000000000..c95722f4e3 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs @@ -0,0 +1,25 @@ +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models.Identity +{ + public interface IIdentityUserLogin : IAggregateRoot, IRememberBeingDirty, ICanBeDirty + { + /// + /// The login provider for the login (i.e. facebook, google) + /// + /// + string LoginProvider { get; set; } + + /// + /// Key representing the login for the provider + /// + /// + string ProviderKey { get; set; } + + /// + /// User Id for the user who owns this login + /// + /// + int UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs new file mode 100644 index 0000000000..09306bb1f0 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Models.Identity +{ + /// + /// Default IUser implementation + /// + /// + /// + /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want + /// references to that so we will create our own here + /// + public class IdentityUser : IUser + where TLogin : IIdentityUserLogin + //NOTE: Making our role id a string + where TRole : IdentityUserRole + where TClaim : IdentityUserClaim + { + /// + /// Email + /// + /// + public virtual string Email { get; set; } + + /// + /// True if the email is confirmed, default is false + /// + /// + public virtual bool EmailConfirmed { get; set; } + + /// + /// The salted/hashed form of the user password + /// + /// + public virtual string PasswordHash { get; set; } + + /// + /// A random value that should change whenever a users credentials have changed (password changed, login removed) + /// + /// + public virtual string SecurityStamp { get; set; } + + /// + /// PhoneNumber for the user + /// + /// + public virtual string PhoneNumber { get; set; } + + /// + /// True if the phone number is confirmed, default is false + /// + /// + public virtual bool PhoneNumberConfirmed { get; set; } + + /// + /// Is two factor enabled for the user + /// + /// + public virtual bool TwoFactorEnabled { get; set; } + + /// + /// DateTime in UTC when lockout ends, any time in the past is considered not locked out. + /// + /// + public virtual DateTime? LockoutEndDateUtc { get; set; } + + /// + /// Is lockout enabled for this user + /// + /// + public virtual bool LockoutEnabled { get; set; } + + /// + /// Used to record failures for the purposes of lockout + /// + /// + public virtual int AccessFailedCount { get; set; } + + /// + /// Navigation property for user roles + /// + /// + public virtual ICollection Roles { get; private set; } + + /// + /// Navigation property for user claims + /// + /// + public virtual ICollection Claims { get; private set; } + + /// + /// Navigation property for user logins + /// + /// + public virtual ICollection Logins { get; private set; } + + /// + /// User ID (Primary Key) + /// + /// + public virtual TKey Id { get; set; } + + /// + /// User name + /// + /// + public virtual string UserName { get; set; } + + /// + /// Constructor + /// + /// + public IdentityUser() + { + this.Claims = new List(); + this.Roles = new List(); + this.Logins = new List(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs b/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs new file mode 100644 index 0000000000..832438f3c3 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IdentityUserClaim.cs @@ -0,0 +1,38 @@ +namespace Umbraco.Core.Models.Identity +{ + /// + /// EntityType that represents one specific user claim + /// + /// + /// + /// + /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want + /// references to that so we will create our own here + /// + public class IdentityUserClaim + { + /// + /// Primary key + /// + /// + public virtual int Id { get; set; } + + /// + /// User Id for the user who owns this login + /// + /// + public virtual TKey UserId { get; set; } + + /// + /// Claim type + /// + /// + public virtual string ClaimType { get; set; } + + /// + /// Claim value + /// + /// + public virtual string ClaimValue { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs new file mode 100644 index 0000000000..f9c77031e9 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs @@ -0,0 +1,39 @@ +using System; +using Umbraco.Core.Models.EntityBase; + +namespace Umbraco.Core.Models.Identity +{ + /// + /// Entity type for a user's login (i.e. facebook, google) + /// + /// + public class IdentityUserLogin : Entity, IIdentityUserLogin + { + public IdentityUserLogin(int id, string loginProvider, string providerKey, int userId, DateTime createDate) + { + Id = id; + LoginProvider = loginProvider; + ProviderKey = providerKey; + UserId = userId; + CreateDate = createDate; + } + + /// + /// The login provider for the login (i.e. facebook, google) + /// + /// + public string LoginProvider { get; set; } + + /// + /// Key representing the login for the provider + /// + /// + public string ProviderKey { get; set; } + + /// + /// User Id for the user who owns this login + /// + /// + public int UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs b/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs new file mode 100644 index 0000000000..5c68a97d86 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/IdentityUserRole.cs @@ -0,0 +1,26 @@ +namespace Umbraco.Core.Models.Identity +{ + /// + /// EntityType that represents a user belonging to a role + /// + /// + /// + /// + /// This class normally exists inside of the EntityFramework library, not sure why MS chose to explicitly put it there but we don't want + /// references to that so we will create our own here + /// + public class IdentityUserRole + { + /// + /// UserId for the user that is in the role + /// + /// + public virtual TKey UserId { get; set; } + + /// + /// RoleId for the role + /// + /// + public virtual TKey RoleId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Rdbms/ExternalLoginDto.cs b/src/Umbraco.Core/Models/Rdbms/ExternalLoginDto.cs new file mode 100644 index 0000000000..c94ee1193f --- /dev/null +++ b/src/Umbraco.Core/Models/Rdbms/ExternalLoginDto.cs @@ -0,0 +1,35 @@ +using System; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.DatabaseAnnotations; + +namespace Umbraco.Core.Models.Rdbms +{ + [TableName("umbracoExternalLogin")] + [ExplicitColumns] + [PrimaryKey("externalLoginId")] + internal class ExternalLoginDto + { + [Column("id")] + [PrimaryKeyColumn(Name = "PK_umbracoExternalLogin")] + public int Id { get; set; } + + [Column("userId")] + public int UserId { get; set; } + + [Column("loginProvider")] + [Length(4000)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string LoginProvider { get; set; } + + [Column("providerKey")] + [Length(4000)] + [NullSetting(NullSetting = NullSettings.NotNull)] + public string ProviderKey { get; set; } + + [Column("createDate")] + [Constraint(Default = "getdate()")] + public DateTime CreateDate { get; set; } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs new file mode 100644 index 0000000000..4882df3202 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs @@ -0,0 +1,32 @@ +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Rdbms; + +namespace Umbraco.Core.Persistence.Factories +{ + internal class ExternalLoginFactory + { + public IIdentityUserLogin BuildEntity(ExternalLoginDto dto) + { + var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId, dto.CreateDate); + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + entity.ResetDirtyProperties(false); + return entity; + } + + public ExternalLoginDto BuildDto(IIdentityUserLogin entity) + { + var dto = new ExternalLoginDto + { + Id = entity.Id, + CreateDate = entity.CreateDate, + LoginProvider = entity.LoginProvider, + ProviderKey = entity.ProviderKey, + UserId = entity.UserId + }; + + return dto; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index b77e0843ea..25d5f2a9af 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -81,7 +81,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {40, typeof (ServerRegistrationDto)}, {41, typeof (AccessDto)}, - {42, typeof (AccessRuleDto)} + {42, typeof (AccessRuleDto)}, + + {43, typeof (ExternalLoginDto)} }; #endregion diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs new file mode 100644 index 0000000000..b29aff9048 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddExternalLoginsTable.cs @@ -0,0 +1,28 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero +{ + [Migration("7.3.0", 9, GlobalSettings.UmbracoMigrationName)] + public class AddExternalLoginsTable : MigrationBase + { + public override void Up() + { + //Don't exeucte if the table is already there + var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray(); + if (tables.InvariantContains("umbracoExternalLogin")) return; + + Create.Table("umbracoExternalLogin") + .WithColumn("id").AsInt32().Identity().PrimaryKey("PK_umbracoExternalLogin") + .WithColumn("userId").AsInt32().NotNullable().ForeignKey("FK_umbracoExternalLogin_umbracoUser_id", "umbracoUser", "id") + .WithColumn("loginProvider").AsString(4000).NotNullable() + .WithColumn("providerKey").AsString(4000).NotNullable() + .WithColumn("createDate").AsDateTime().NotNullable().WithDefault(SystemMethods.CurrentDateTime); + } + + public override void Down() + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs new file mode 100644 index 0000000000..bafe30d901 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/ExternalLoginRepository.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.Factories; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.SqlSyntax; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Persistence.Repositories +{ + internal class ExternalLoginRepository : PetaPocoRepositoryBase, IExternalLoginRepository + { + public ExternalLoginRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cache, logger, sqlSyntax) + { + } + + public void DeleteUserLogins(int memberId) + { + using (var t = Database.GetTransaction()) + { + Database.Execute("DELETE FROM ExternalLogins WHERE UserId=@userId", new { userId = memberId }); + + t.Complete(); + } + } + + public void SaveUserLogins(int memberId, IEnumerable logins) + { + using (var t = Database.GetTransaction()) + { + //clear out logins for member + Database.Execute("DELETE FROM ExternalLogins WHERE UserId=@userId", new { userId = memberId }); + + //add them all + foreach (var l in logins) + { + Database.Insert(new ExternalLoginDto + { + LoginProvider = l.LoginProvider, + ProviderKey = l.ProviderKey, + UserId = memberId, + CreateDate = DateTime.Now + }); + } + + t.Complete(); + } + } + + protected override IIdentityUserLogin PerformGet(int id) + { + var sql = GetBaseQuery(false); + sql.Where(GetBaseWhereClause(), new { Id = id }); + + var macroDto = Database.Fetch(sql).FirstOrDefault(); + if (macroDto == null) + return null; + + var factory = new ExternalLoginFactory(); + var entity = factory.BuildEntity(macroDto); + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((TracksChangesEntityBase)entity).ResetDirtyProperties(false); + + return entity; + } + + protected override IEnumerable PerformGetAll(params int[] ids) + { + if (ids.Any()) + { + return PerformGetAllOnIds(ids); + } + + var sql = GetBaseQuery(false); + + return ConvertFromDtos(Database.Fetch(sql)) + .ToArray();// we don't want to re-iterate again! + } + + private IEnumerable PerformGetAllOnIds(params int[] ids) + { + if (ids.Any() == false) yield break; + foreach (var id in ids) + { + yield return Get(id); + } + } + + private IEnumerable ConvertFromDtos(IEnumerable dtos) + { + var factory = new ExternalLoginFactory(); + foreach (var entity in dtos.Select(factory.BuildEntity)) + { + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((TracksChangesEntityBase)entity).ResetDirtyProperties(false); + + yield return entity; + } + } + + protected override IEnumerable PerformGetByQuery(IQuery query) + { + var sqlClause = GetBaseQuery(false); + var translator = new SqlTranslator(sqlClause, query); + var sql = translator.Translate(); + + var dtos = Database.Fetch(sql); + + foreach (var dto in dtos) + { + yield return Get(dto.Id); + } + } + + protected override Sql GetBaseQuery(bool isCount) + { + var sql = new Sql(); + if (isCount) + { + sql.Select("COUNT(*)").From(SqlSyntax); + } + else + { + sql.Select("*").From(SqlSyntax); + } + return sql; + } + + protected override string GetBaseWhereClause() + { + return "umbracoExternalLogin.id = @Id"; + } + + protected override IEnumerable GetDeleteClauses() + { + var list = new List + { + "DELETE FROM umbracoExternalLogin WHERE id = @Id" + }; + return list; + } + + protected override Guid NodeObjectTypeId + { + get { throw new NotImplementedException(); } + } + + protected override void PersistNewItem(IIdentityUserLogin entity) + { + ((Entity)entity).AddingEntity(); + + var factory = new ExternalLoginFactory(); + var dto = factory.BuildDto(entity); + + var id = Convert.ToInt32(Database.Insert(dto)); + entity.Id = id; + + entity.ResetDirtyProperties(); + } + + protected override void PersistUpdatedItem(IIdentityUserLogin entity) + { + ((Entity)entity).UpdatingEntity(); + + var factory = new ExternalLoginFactory(); + var dto = factory.BuildDto(entity); + + Database.Update(dto); + + entity.ResetDirtyProperties(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IExternalLoginRepository.cs new file mode 100644 index 0000000000..17fc153980 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IExternalLoginRepository.cs @@ -0,0 +1,12 @@ +using System.Collections.Generic; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IExternalLoginRepository : IRepositoryQueryable + { + void SaveUserLogins(int memberId, IEnumerable logins); + void DeleteUserLogins(int memberId); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index fb66dc7ade..d13df47b71 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -122,7 +122,8 @@ namespace Umbraco.Core.Persistence.Repositories "DELETE FROM umbracoUser2NodeNotify WHERE userId = @Id", "DELETE FROM umbracoUserLogins WHERE userID = @Id", "DELETE FROM umbracoUser2app WHERE " + SqlSyntax.GetQuotedColumnName("user") + "=@Id", - "DELETE FROM umbracoUser WHERE id = @Id" + "DELETE FROM umbracoUser WHERE id = @Id", + "DELETE FROM umbracoExternalLogin WHERE id = @Id" }; return list; } diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index be6d35ca27..356e876c54 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -64,6 +64,13 @@ namespace Umbraco.Core.Persistence #endregion + public virtual IExternalLoginRepository CreateExternalLoginRepository(IDatabaseUnitOfWork uow) + { + return new ExternalLoginRepository(uow, + _cacheHelper, + _logger, _sqlSyntax); + } + public virtual IPublicAccessRepository CreatePublicAccessRepository(IDatabaseUnitOfWork uow) { return new PublicAccessRepository(uow, diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index dd688cfe25..e71dd0e00c 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -216,7 +216,6 @@ namespace Umbraco.Core.Security GlobalSettings.TimeOutInMinutes, //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way 1440, - "/umbraco", UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain); } @@ -420,7 +419,6 @@ namespace Umbraco.Core.Security string userData, int loginTimeoutMins, int minutesPersisted, - string cookiePath, string cookieName, string cookieDomain) { @@ -433,7 +431,7 @@ namespace Umbraco.Core.Security DateTime.Now.AddMinutes(loginTimeoutMins), true, userData, - cookiePath + "/" ); // Encrypt the cookie using the machine key for secure transport @@ -444,7 +442,7 @@ namespace Umbraco.Core.Security { Expires = DateTime.Now.AddMinutes(minutesPersisted), Domain = cookieDomain, - Path = cookiePath + Path = "/" }; if (GlobalSettings.UseSSL) diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs new file mode 100644 index 0000000000..54b537faab --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + public class BackOfficeClaimsIdentityFactory : ClaimsIdentityFactory + { + /// + /// Create a ClaimsIdentity from a user + /// + /// + /// + public override async Task CreateAsync(UserManager manager, BackOfficeIdentityUser user, string authenticationType) + { + var baseIdentity = await base.CreateAsync(manager, user, authenticationType); + + var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity, new UserData() + { + Id = user.Id, + Username = user.UserName, + RealName = user.Name, + AllowedApplications = user.AllowedApplications, + Culture = user.Culture, + Roles = user.Roles.Select(x => x.RoleId).ToArray(), + StartContentNode = user.StartContentNode, + StartMediaNode = user.StartMediaNode + }); + + return umbracoIdentity; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs new file mode 100644 index 0000000000..9634b17c73 --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -0,0 +1,157 @@ +using System; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Web.Security; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Security +{ + /// + /// A custom password hasher that conforms to the current password hashing done in Umbraco + /// + internal class MembershipPasswordHasher : IPasswordHasher + { + private readonly MembershipProviderBase _provider; + + public MembershipPasswordHasher(MembershipProviderBase provider) + { + _provider = provider; + } + + public string HashPassword(string password) + { + return _provider.HashPasswordForStorage(password); + } + + public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) + { + return _provider.VerifyPassword(providedPassword, hashedPassword) + ? PasswordVerificationResult.Success + : PasswordVerificationResult.Failed; + } + + + } + + /// + /// Back office user manager + /// + public class BackOfficeUserManager : UserManager + { + public BackOfficeUserManager(IUserStore store) + : base(store) + { + } + + #region What we support currently + + //TODO: Support this + public override bool SupportsUserRole + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsQueryableUsers + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsUserLockout + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsUserSecurityStamp + { + get { return false; } + } + + public override bool SupportsUserTwoFactor + { + get { return false; } + } + + public override bool SupportsUserPhoneNumber + { + get { return false; } + } + #endregion + + public static BackOfficeUserManager Create( + IdentityFactoryOptions options, + IOwinContext context, + IUserService userService, + IExternalLoginService externalLoginService, + MembershipProviderBase membershipProvider) + { + if (options == null) throw new ArgumentNullException("options"); + if (context == null) throw new ArgumentNullException("context"); + if (userService == null) throw new ArgumentNullException("userService"); + if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); + + var manager = new BackOfficeUserManager(new BackOfficeUserStore(userService, externalLoginService, membershipProvider)); + + // Configure validation logic for usernames + manager.UserValidator = new UserValidator(manager) + { + AllowOnlyAlphanumericUserNames = false, + RequireUniqueEmail = true + }; + + // Configure validation logic for passwords + manager.PasswordValidator = new PasswordValidator + { + RequiredLength = membershipProvider.MinRequiredPasswordLength, + RequireNonLetterOrDigit = membershipProvider.MinRequiredNonAlphanumericCharacters > 0, + RequireDigit = false, + RequireLowercase = false, + RequireUppercase = false + }; + + //use a custom hasher based on our membership provider + manager.PasswordHasher = new MembershipPasswordHasher(membershipProvider); + + var dataProtectionProvider = options.DataProtectionProvider; + if (dataProtectionProvider != null) + { + manager.UserTokenProvider = new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")); + } + + //custom identity factory for creating the identity object for which we auth against in the back office + manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); + + //NOTE: Not implementing these currently + + //// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user + //// You can write your own provider and plug in here. + //manager.RegisterTwoFactorProvider("PhoneCode", new PhoneNumberTokenProvider + //{ + // MessageFormat = "Your security code is: {0}" + //}); + //manager.RegisterTwoFactorProvider("EmailCode", new EmailTokenProvider + //{ + // Subject = "Security Code", + // BodyFormat = "Your security code is: {0}" + //}); + + //manager.EmailService = new EmailService(); + //manager.SmsService = new SmsService(); + + return manager; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + } + } +} diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs new file mode 100644 index 0000000000..427bc306da --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Web.Security; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Services; + +namespace Umbraco.Core.Security +{ + public class BackOfficeUserStore : DisposableObject, IUserStore, IUserPasswordStore, IUserEmailStore, IUserLoginStore + { + private readonly IUserService _userService; + private readonly IExternalLoginService _externalLoginService; + private readonly MembershipProviderBase _usersMembershipProvider; + + public BackOfficeUserStore(IUserService userService, IExternalLoginService externalLoginService, MembershipProviderBase usersMembershipProvider) + { + _userService = userService; + _externalLoginService = externalLoginService; + _usersMembershipProvider = usersMembershipProvider; + if (userService == null) throw new ArgumentNullException("userService"); + if (usersMembershipProvider == null) throw new ArgumentNullException("usersMembershipProvider"); + if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); + + _userService = userService; + _usersMembershipProvider = usersMembershipProvider; + _externalLoginService = externalLoginService; + + if (_usersMembershipProvider.PasswordFormat != MembershipPasswordFormat.Hashed) + { + throw new InvalidOperationException("Cannot use ASP.Net Identity with UmbracoMembersUserStore when the password format is not Hashed"); + } + } + + /// + /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// + protected override void DisposeResources() + { + throw new NotImplementedException(); + } + + /// + /// Insert a new user + /// + /// + /// + public Task CreateAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Update a user + /// + /// + /// + public Task UpdateAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Delete a user + /// + /// + /// + public Task DeleteAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Finds a user + /// + /// + /// + public Task FindByIdAsync(int userId) + { + throw new NotImplementedException(); + } + + /// + /// Find a user by name + /// + /// + /// + public Task FindByNameAsync(string userName) + { + throw new NotImplementedException(); + } + + /// + /// Set the user password hash + /// + /// + /// + public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash) + { + throw new NotImplementedException(); + } + + /// + /// Get the user password hash + /// + /// + /// + public Task GetPasswordHashAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Returns true if a user has a password set + /// + /// + /// + public Task HasPasswordAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Set the user email + /// + /// + /// + public Task SetEmailAsync(BackOfficeIdentityUser user, string email) + { + throw new NotImplementedException(); + } + + /// + /// Get the user email + /// + /// + /// + public Task GetEmailAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Returns true if the user email is confirmed + /// + /// + /// + public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Sets whether the user email is confirmed + /// + /// + /// + public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed) + { + throw new NotImplementedException(); + } + + /// + /// Returns the user associated with this email + /// + /// + /// + public Task FindByEmailAsync(string email) + { + throw new NotImplementedException(); + } + + /// + /// Adds a user login with the specified provider and key + /// + /// + /// + public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) + { + throw new NotImplementedException(); + } + + /// + /// Removes the user login with the specified combination if it exists + /// + /// + /// + public Task RemoveLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) + { + throw new NotImplementedException(); + } + + /// + /// Returns the linked accounts for this user + /// + /// + /// + public Task> GetLoginsAsync(BackOfficeIdentityUser user) + { + throw new NotImplementedException(); + } + + /// + /// Returns the user associated with this login + /// + /// + public Task FindAsync(UserLoginInfo login) + { + //get all logins associated with the login id + var result = _externalLoginService.Find(login).ToArray(); + if (result.Any()) + { + //return the first member that matches the result + var user = (from l in result + select _userService.GetUserById(l.Id) + into member + where member != null + select new BackOfficeIdentityUser + { + Email = member.Email, + Id = member.Id, + LockoutEnabled = member.IsLockedOut, + LockoutEndDateUtc = DateTime.MaxValue.ToUniversalTime(), + UserName = member.Username, + PasswordHash = GetPasswordHash(member.RawPasswordValue) + }).FirstOrDefault(); + + return Task.FromResult(AssignLoginsCallback(user)); + } + + return Task.FromResult(null); + } + + private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user) + { + if (user != null) + { + user.SetLoginsCallback(new Lazy>(() => + _externalLoginService.GetAll(user.Id))); + } + return user; + } + + private string GetPasswordHash(string storedPass) + { + return storedPass.StartsWith("___UIDEMPTYPWORD__") ? null : storedPass; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index ebcd967cc2..e39919d291 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -16,6 +16,19 @@ namespace Umbraco.Core.Security /// public abstract class MembershipProviderBase : MembershipProvider { + + public string HashPasswordForStorage(string password) + { + string salt; + var hashed = EncryptOrHashNewPassword(password, out salt); + return FormatPasswordForStorage(hashed, salt); + } + + public bool VerifyPassword(string password, string hashedPassword) + { + return CheckPassword(password, hashedPassword); + } + /// /// Providers can override this setting, default is 7 /// diff --git a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs index f59f4d1169..bdd3174960 100644 --- a/src/Umbraco.Core/Security/MembershipProviderExtensions.cs +++ b/src/Umbraco.Core/Security/MembershipProviderExtensions.cs @@ -13,21 +13,21 @@ using Umbraco.Core.Security; namespace Umbraco.Core.Security { - internal static class MembershipProviderExtensions + public static class MembershipProviderExtensions { - public static MembershipUserCollection FindUsersByName(this MembershipProvider provider, string usernameToMatch) + internal static MembershipUserCollection FindUsersByName(this MembershipProvider provider, string usernameToMatch) { int totalRecords = 0; return provider.FindUsersByName(usernameToMatch, 0, int.MaxValue, out totalRecords); } - public static MembershipUserCollection FindUsersByEmail(this MembershipProvider provider, string emailToMatch) + internal static MembershipUserCollection FindUsersByEmail(this MembershipProvider provider, string emailToMatch) { int totalRecords = 0; return provider.FindUsersByEmail(emailToMatch, 0, int.MaxValue, out totalRecords); } - public static MembershipUser CreateUser(this MembershipProvider provider, string username, string password, string email) + internal static MembershipUser CreateUser(this MembershipProvider provider, string username, string password, string email) { MembershipCreateStatus status; var user = provider.CreateUser(username, password, email, null, null, true, null, out status); @@ -80,7 +80,7 @@ namespace Umbraco.Core.Security /// /// /// - public static MembershipUser GetCurrentUser(this MembershipProvider membershipProvider) + internal static MembershipUser GetCurrentUser(this MembershipProvider membershipProvider) { var username = membershipProvider.GetCurrentUserName(); return username.IsNullOrWhiteSpace() @@ -93,7 +93,7 @@ namespace Umbraco.Core.Security /// /// /// - public static string GetCurrentUserName(this MembershipProvider membershipProvider) + internal static string GetCurrentUserName(this MembershipProvider membershipProvider) { if (HostingEnvironment.IsHosted) { diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index be6811ad7b..08f53fb76e 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -27,8 +27,28 @@ namespace Umbraco.Core.Security //This just creates a temp/fake ticket : base(new FormsAuthenticationTicket(userdata.Username, true, 10)) { + if (userdata == null) throw new ArgumentNullException("userdata"); UserData = userdata; - AddClaims(); + AddUserDataClaims(); + } + + /// + /// Create a back office identity based on an existing claims identity + /// + /// + /// + public UmbracoBackOfficeIdentity(ClaimsIdentity claimsIdentity, UserData userdata) + //This just creates a temp/fake ticket + : base(new FormsAuthenticationTicket(userdata.Username, true, 10)) + { + if (claimsIdentity == null) throw new ArgumentNullException("claimsIdentity"); + if (userdata == null) throw new ArgumentNullException("userdata"); + + _currentIssuer = claimsIdentity.AuthenticationType; + UserData = userdata; + AddClaims(claimsIdentity); + Actor = claimsIdentity; + AddUserDataClaims(); } /// @@ -39,7 +59,7 @@ namespace Umbraco.Core.Security : base(ticket) { UserData = JsonConvert.DeserializeObject(ticket.UserData); - AddClaims(); + AddUserDataClaims(); } /// @@ -49,24 +69,49 @@ namespace Umbraco.Core.Security private UmbracoBackOfficeIdentity(UmbracoBackOfficeIdentity identity) : base(identity) { + if (identity.Actor != null) + { + _currentIssuer = identity.AuthenticationType; + AddClaims(identity); + Actor = identity.Clone(); + } + UserData = identity.UserData; - AddClaims(); + AddUserDataClaims(); } - public static string Issuer = "UmbracoBackOffice"; + public const string Issuer = "UmbracoBackOffice"; + private readonly string _currentIssuer = Issuer; - //TODO: Another option is to create a ClaimsIdentityFactory when everything is wired up... optional though i think - private void AddClaims() + private void AddClaims(ClaimsIdentity claimsIdentity) + { + foreach (var claim in claimsIdentity.Claims) + { + AddClaim(claim); + } + } + + /// + /// Adds claims based on the UserData data + /// + private void AddUserDataClaims() { AddClaims(new[] { new Claim(Constants.Security.StartContentNodeIdClaimType, StartContentNode.ToInvariantString(), null, Issuer, Issuer, this), new Claim(Constants.Security.StartMediaNodeIdClaimType, StartMediaNode.ToInvariantString(), null, Issuer, Issuer, this), new Claim(Constants.Security.AllowedApplicationsClaimType, string.Join(",", AllowedApplications), null, Issuer, Issuer, this), + + //TODO: Similar one created by the ClaimsIdentityFactory not sure we need this new Claim(Constants.Security.UserIdClaimType, Id.ToString(), null, Issuer, Issuer, this), new Claim(Constants.Security.CultureClaimType, Culture, null, Issuer, Issuer, this), new Claim(Constants.Security.SessionIdClaimType, SessionId, null, Issuer, Issuer, this), - new Claim(ClaimTypes.Role, string.Join(",", Roles), null, Issuer, Issuer, this) + + //TODO: Role claims are added by the default ClaimsIdentityFactory based on the result from + // the user manager manager.GetRolesAsync method so not sure if we can do that there or needs to be done here + // and each role should be a different claim, not a single string + + //new Claim(ClaimTypes.Role, string.Join(",", Roles), null, Issuer, Issuer, this) }); } @@ -80,7 +125,7 @@ namespace Umbraco.Core.Security /// public override string AuthenticationType { - get { return Issuer; } + get { return _currentIssuer; } } public int StartContentNode diff --git a/src/Umbraco.Core/Security/UmbracoMembershipProviderBase.cs b/src/Umbraco.Core/Security/UmbracoMembershipProviderBase.cs index 12ca2b4b4a..328d8ad335 100644 --- a/src/Umbraco.Core/Security/UmbracoMembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/UmbracoMembershipProviderBase.cs @@ -8,6 +8,8 @@ namespace Umbraco.Core.Security /// public abstract class UmbracoMembershipProviderBase : MembershipProviderBase { + + public abstract string DefaultMemberTypeAlias { get; } /// diff --git a/src/Umbraco.Core/Services/ExternalLoginService.cs b/src/Umbraco.Core/Services/ExternalLoginService.cs new file mode 100644 index 0000000000..f33f1c492b --- /dev/null +++ b/src/Umbraco.Core/Services/ExternalLoginService.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.UnitOfWork; + +namespace Umbraco.Core.Services +{ + public class ExternalLoginService : RepositoryService, IExternalLoginService + { + public ExternalLoginService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger) + : base(provider, repositoryFactory, logger) + { + } + + /// + /// Returns all user logins assigned + /// + /// + /// + public IEnumerable GetAll(int userId) + { + using (var repo = RepositoryFactory.CreateExternalLoginRepository(UowProvider.GetUnitOfWork())) + { + return repo.GetByQuery(new Query().Where(x => x.UserId == userId)); + } + } + + /// + /// Returns all logins matching the login info - generally there should only be one but in some cases + /// there might be more than one depending on if an adminstrator has been editing/removing members + /// + /// + /// + public IEnumerable Find(UserLoginInfo login) + { + using (var repo = RepositoryFactory.CreateExternalLoginRepository(UowProvider.GetUnitOfWork())) + { + return repo.GetByQuery(new Query() + .Where(x => x.ProviderKey == login.ProviderKey && x.LoginProvider == login.LoginProvider)); + } + } + + /// + /// Save user logins + /// + /// + /// + public void SaveUserLogins(int userId, IEnumerable logins) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateExternalLoginRepository(uow)) + { + repo.SaveUserLogins(userId, logins); + uow.Commit(); + } + } + + /// + /// Deletes all user logins - normally used when a member is deleted + /// + /// + public void DeleteUserLogins(int userId) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repo = RepositoryFactory.CreateExternalLoginRepository(uow)) + { + repo.DeleteUserLogins(userId); + uow.Commit(); + } + } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IExternalLoginService.cs b/src/Umbraco.Core/Services/IExternalLoginService.cs new file mode 100644 index 0000000000..e1b1a161d8 --- /dev/null +++ b/src/Umbraco.Core/Services/IExternalLoginService.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Services +{ + /// + /// Used to store the external login info, this can be replaced with your own implementation + /// + public interface IExternalLoginService : IService + { + /// + /// Returns all user logins assigned + /// + /// + /// + IEnumerable GetAll(int userId); + + /// + /// Returns all logins matching the login info - generally there should only be one but in some cases + /// there might be more than one depending on if an adminstrator has been editing/removing members + /// + /// + /// + IEnumerable Find(UserLoginInfo login); + + /// + /// Save user logins + /// + /// + /// + void SaveUserLogins(int userId, IEnumerable logins); + + /// + /// Deletes all user logins - normally used when a member is deleted + /// + /// + void DeleteUserLogins(int userId); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs index 484226ae78..7803abe98e 100644 --- a/src/Umbraco.Core/Services/IFileService.cs +++ b/src/Umbraco.Core/Services/IFileService.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Umbraco.Core.Models; diff --git a/src/Umbraco.Core/Services/ServiceContext.cs b/src/Umbraco.Core/Services/ServiceContext.cs index f1eb873db7..d72115e49d 100644 --- a/src/Umbraco.Core/Services/ServiceContext.cs +++ b/src/Umbraco.Core/Services/ServiceContext.cs @@ -41,6 +41,7 @@ namespace Umbraco.Core.Services private Lazy _memberTypeService; private Lazy _memberGroupService; private Lazy _notificationService; + private Lazy _externalLoginService; /// /// public ctor - will generally just be used for unit testing all items are optional and if not specified, the defaults will be used @@ -91,8 +92,10 @@ namespace Umbraco.Core.Services IDomainService domainService = null, ITaskService taskService = null, IMacroService macroService = null, - IPublicAccessService publicAccessService = null) + IPublicAccessService publicAccessService = null, + IExternalLoginService externalLoginService = null) { + if (externalLoginService != null) _externalLoginService = new Lazy(() => externalLoginService); if (auditService != null) _auditService = new Lazy(() => auditService); if (localizedTextService != null) _localizedTextService = new Lazy(() => localizedTextService); if (tagService != null) _tagService = new Lazy(() => tagService); @@ -145,6 +148,9 @@ namespace Umbraco.Core.Services var provider = dbUnitOfWorkProvider; var fileProvider = fileUnitOfWorkProvider; + if (_externalLoginService == null) + _externalLoginService = new Lazy(() => new ExternalLoginService(provider, repositoryFactory, logger)); + if (_publicAccessService == null) _publicAccessService = new Lazy(() => new PublicAccessService(provider, repositoryFactory, logger)); @@ -415,5 +421,9 @@ namespace Umbraco.Core.Services get { return _memberGroupService.Value; } } + public IExternalLoginService ExternalLoginService + { + get { return _externalLoginService.Value; } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index f0f09c499c..f76cb2667c 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -67,12 +67,13 @@ False ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll - - ..\packages\Microsoft.Owin.Security.Cookies.2.1.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + + False + ..\packages\Microsoft.Owin.Security.Cookies.3.0.0\lib\net45\Microsoft.Owin.Security.Cookies.dll - - ..\packages\Microsoft.Owin.Security.OAuth.2.1.0\lib\net45\Microsoft.Owin.Security.OAuth.dll - True + + False + ..\packages\Microsoft.Owin.Security.OAuth.3.0.0\lib\net45\Microsoft.Owin.Security.OAuth.dll True @@ -347,6 +348,8 @@ + + @@ -368,11 +371,13 @@ + + @@ -400,8 +405,10 @@ + + @@ -409,7 +416,9 @@ + + @@ -420,8 +429,10 @@ + + diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index 93c9737158..e8702a60bc 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -14,8 +14,8 @@ - - + + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js index 00d3cda1ab..04e6672b4b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.controller.js @@ -17,9 +17,8 @@ $scope.errorMsg = ""; - $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; - - $scope.externalLogins = Umbraco.Sys.ServerVariables.externalLogins; + $scope.externalLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLoginsUrl; + $scope.externalLoginProviders = Umbraco.Sys.ServerVariables.externalLogins.providers; $scope.loginSubmit = function (login, password) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 5b73743772..b09c11bf18 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -23,12 +23,12 @@
{{errorMsg}}
- +

-
+
- -

+ +
+ +

External login providers

+ +
+ {{error}} +
-
+
- - -
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js index 5979962128..c6531e6016 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js @@ -1,10 +1,12 @@ angular.module("umbraco") - .controller("Umbraco.Dialogs.UserController", function ($scope, $location, $timeout, userService, historyService, eventsService) { + .controller("Umbraco.Dialogs.UserController", function ($scope, $location, $timeout, userService, historyService, eventsService, externalLoginInfo, authResource) { - $scope.user = userService.getCurrentUser(); $scope.history = historyService.getCurrent(); $scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; + $scope.externalLoginProviders = externalLoginInfo.providers; + $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; + var evtHandlers = []; evtHandlers.push(eventsService.on("historyService.add", function (e, args) { $scope.history = args.all; @@ -49,6 +51,20 @@ angular.module("umbraco") }, 1000, false); // 1 second, do NOT execute a global digest } + $scope.unlink = function(e, loginProvider, providerKey) { + var result = confirm("Are you sure you want to unlink this account?"); + if (!result) { + e.preventDefault(); + return; + } + + authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { + var asdf = ";" + }, function(err) { + var asdf = err; + }); + } + //get the user userService.getCurrentUser().then(function (user) { $scope.user = user; @@ -57,6 +73,16 @@ angular.module("umbraco") $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; //set the timer updateTimeout(); + + //set the linked logins + for (var login in $scope.user.linkedLogins) { + var found = _.find($scope.externalLoginProviders, function(i) { + return i.authType == login; + }); + if (found) { + found.linkedProviderKey = $scope.user.linkedLogins[login]; + } + } } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html index ff71cec524..c73de2bb3a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html @@ -1,48 +1,76 @@
-
-
-
- -
-
-

{{user.name}}

-

- - : {{remainingAuthSeconds | timespan}} -

+
+
+
+ +
+
+

{{user.name}}

+

+ + : {{remainingAuthSeconds | timespan}} + +

-
- -
-
- -
-
-

- - Edit - -

-
+
+
+
-
-
- -
+
+
+

+ + Edit + +

+
-
+
- Umbraco version {{version}} -
+
External login providers
+ +
+ +
+ +
+ + + + {{login.caption}} + +
+ +
+ +
+
+ +
+ +
+ + Umbraco version {{version}} +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index baf56c1bb5..a11e071aa3 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -63,7 +63,9 @@ namespace Umbraco.Web.UI //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); - + app.UseGoogleAuthentication( + clientId: "1072120697051-07jlhgrd5hodsfe7dgqimdie8qc1omet.apps.googleusercontent.com", + clientSecret: "Ue9swN0lEX9rwxzQz1Y_tFzg"); } diff --git a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml index 55c28da1f3..c51e1c6ab9 100644 --- a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml @@ -1,4 +1,5 @@ -@using System.Net.Http +@using System.Collections +@using System.Net.Http @using System.Web.Mvc.Html @using Umbraco.Core @using ClientDependency.Core @@ -52,9 +53,13 @@ @{ var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes() - .Select(p => new {authType = p.AuthenticationType, caption = p.Caption, + .Select(p => new + { + authType = p.AuthenticationType, + caption = p.Caption, //TODO: Need to see if this exposes any sensitive data! - properties = p.Properties}) + properties = p.Properties + }) .ToArray(); } @@ -63,23 +68,42 @@ we will load the rest of the server vars after the user is authenticated. *@ + + + + @*And finally we can load in our angular app*@ @@ -104,4 +128,3 @@ - diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index e3f13d46f3..f6fcb2c0e7 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -1,8 +1,11 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; +using System.Security.Claims; using System.Text; +using System.Threading.Tasks; using System.Web; using System.Web.Helpers; using System.Web.Http; @@ -24,6 +27,9 @@ using Umbraco.Web.Security; using Umbraco.Web.WebApi; using Umbraco.Web.WebApi.Filters; using umbraco.providers; +using Microsoft.AspNet.Identity.Owin; +using Newtonsoft.Json.Linq; +using Umbraco.Core.Models.Identity; namespace Umbraco.Web.Editors { @@ -58,6 +64,54 @@ namespace Umbraco.Web.Editors throw new NotSupportedException("An HttpContext is required for this request"); } + [WebApi.UmbracoAuthorize] + [ValidateAngularAntiForgeryToken] + public async Task PostUnLinkLogin(UnLinkLoginModel unlinkLoginModel) + { + var result = await UserManager.RemoveLoginAsync( + User.Identity.GetUserId(), + new UserLoginInfo(unlinkLoginModel.LoginProvider, unlinkLoginModel.ProviderKey)); + + if (result.Succeeded) + { + var user = await UserManager.FindByIdAsync(User.Identity.GetUserId()); + await SignInAsync(user, isPersistent: false); + return Request.CreateResponse(HttpStatusCode.OK); + } + else + { + AddModelErrors(result); + return Request.CreateValidationErrorResponse(ModelState); + } + } + + private void AddModelErrors(IdentityResult result, string prefix = "") + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(prefix, error); + } + } + + private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) + { + var owinContext = TryGetOwinContext().Result; + + owinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); + + owinContext.Authentication.SignIn( + new AuthenticationProperties() { IsPersistent = isPersistent }, + await GenerateUserIdentityAsync(user)); + } + + private async Task GenerateUserIdentityAsync(BackOfficeIdentityUser user) + { + // NOTE the authenticationType must match the umbraco one + // defined in CookieAuthenticationOptions.AuthenticationType + var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); + return userIdentity; + } + /// /// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest) /// @@ -85,7 +139,7 @@ namespace Umbraco.Web.Editors /// [WebApi.UmbracoAuthorize] [SetAngularAntiForgeryTokens] - public UserDetail GetCurrentUser() + public async Task GetCurrentUser() { var user = Services.UserService.GetUserById(UmbracoContext.Security.GetUserId()); var result = Mapper.Map(user); @@ -95,9 +149,23 @@ namespace Umbraco.Web.Editors //set their remaining seconds result.SecondsUntilTimeout = httpContextAttempt.Result.GetRemainingAuthSeconds(); } + + //now we need to fill in the user's linked logins, we can't do this in the mapper because it has no access to the + // user manager + + var identityUser = await UserManager.FindByIdAsync(user.Id); + result.LinkedLogins = identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); + return result; } + private BackOfficeUserManager _userManager; + + protected BackOfficeUserManager UserManager + { + get { return _userManager ?? (_userManager = TryGetOwinContext().Result.GetUserManager()); } + } + /// /// Logs a user in /// diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 2ee30bc632..47866de5c9 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -52,11 +52,6 @@ namespace Umbraco.Web.Editors { private BackOfficeUserManager _userManager; - protected IOwinContext OwinContext - { - get { return Request.GetOwinContext(); } - } - protected BackOfficeUserManager UserManager { get { return _userManager ?? (_userManager = OwinContext.GetUserManager()); } @@ -70,30 +65,25 @@ namespace Umbraco.Web.Editors { ViewBag.UmbracoPath = GlobalSettings.UmbracoMvcArea; + //check if there's errors in the TempData, assign to view bag and render the view + if (TempData["ExternalSignInError"] != null) + { + ViewBag.ExternalSignInError = TempData["ExternalSignInError"]; + return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); + } + //First check if there's external login info, if there's not proceed as normal var loginInfo = await OwinContext.Authentication.GetExternalLoginInfoAsync( Core.Constants.Security.BackOfficeExternalAuthenticationType); - + if (loginInfo == null) { return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); } - // 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) - { - await SignInAsync(user, isPersistent: false); - //all signed in so just render the view as per normal - return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); - } + //we're just logging in with an external source, not linking accounts + return await ExternalSignInAsync(loginInfo); - //The user hasn't used this login provider so need to display an error, they must link the provider in the user section - // TODO: Or wherever we decide to put that. - - // TODO: Return a real error in one way or another, maybe a different view? - - throw new SecurityNegotiationException("The requested provider " + loginInfo.Login.LoginProvider + " has not been linked to to an account"); } /// @@ -244,6 +234,8 @@ namespace Umbraco.Web.Editors { "umbracoUrls", new Dictionary { + {"externalLoginsUrl", Url.Action("ExternalLogin", "BackOffice")}, + {"externalLinkLoginsUrl", Url.Action("LinkLogin", "BackOffice")}, {"legacyTreeJs", Url.Action("LegacyTreeJs", "BackOffice")}, {"manifestAssetList", Url.Action("GetManifestAssetList", "BackOffice")}, {"gridConfig", Url.Action("GetGridConfig", "BackOffice")}, @@ -326,10 +318,10 @@ namespace Umbraco.Web.Editors controller => controller.Fetch(string.Empty)) }, { - "relationApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( - controller => controller.GetById(0)) - }, - { + "relationApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( + controller => controller.GetById(0)) + }, + { "rteApiBaseUrl", Url.GetUmbracoApiServiceBaseUrl( controller => controller.GetConfiguration()) }, @@ -395,7 +387,24 @@ namespace Umbraco.Web.Editors } }, {"isDebuggingEnabled", HttpContext.IsDebuggingEnabled}, - {"application", GetApplicationState()} + { + "application", GetApplicationState() + }, + { + "externalLogins", new Dictionary + { + { + "providers", HttpContext.GetOwinContext().Authentication.GetExternalAuthenticationTypes() + .Select(p => new + { + authType = p.AuthenticationType, caption = p.Caption, + //TODO: Need to see if this exposes any sensitive data! + properties = p.Properties + }) + .ToArray() + } + } + } }; //cache the result if debugging is disabled @@ -410,22 +419,78 @@ namespace Umbraco.Web.Editors } [HttpPost] - [AllowAnonymous] public ActionResult ExternalLogin(string provider) { // Request a redirect to the external login provider return new ChallengeResult(provider, - Url.Action("Default", "BackOffice", new + Url.Action("Default", "BackOffice")); + } + + [UmbracoAuthorize] + [HttpPost] + public ActionResult LinkLogin(string provider) + { + // Request a redirect to the external login provider to link a login for the current user + return new ChallengeResult(provider, + Url.Action("ExternalLinkLoginCallback", "BackOffice"), + User.Identity.GetUserId()); + } + + + + [HttpGet] + public async Task ExternalLinkLoginCallback() + { + var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync( + Core.Constants.Security.BackOfficeExternalAuthenticationType, + XsrfKey, User.Identity.GetUserId()); + + if (loginInfo == null) + { + //Add error and redirect for it to be displayed + TempData["ExternalSignInError"] = new[] { "An error occurred, could not get external login info" }; + return RedirectToLocal(Url.Action("Default", "BackOffice")); + } + + var result = await UserManager.AddLoginAsync(User.Identity.GetUserId(), loginInfo.Login); + if (result.Succeeded) + { + return RedirectToLocal(Url.Action("Default", "BackOffice")); + } + + //Add errors and redirect for it to be displayed + TempData["ExternalSignInError"] = result.Errors; + return RedirectToLocal(Url.Action("Default", "BackOffice")); + } + + private async Task ExternalSignInAsync(ExternalLoginInfo loginInfo) + { + if (loginInfo == null) throw new ArgumentNullException("loginInfo"); + + // 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) + { + //sign in + await SignInAsync(user, isPersistent: false); + } + else + { + 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.BackOfficeExternalAuthenticationType] != null) { - area = GlobalSettings.UmbracoMvcArea - })); + Response.Cookies[Core.Constants.Security.BackOfficeExternalAuthenticationType].Expires = DateTime.MinValue; + } + } + + return View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml"); } private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) { - //TODO: I don't think we want to reference the 'default' external cookie since people might be using this on the front-end - // we'll need to create a secondary custom handler for the external cookie for the back office - OwinContext.Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); + OwinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); OwinContext.Authentication.SignIn( new AuthenticationProperties() {IsPersistent = isPersistent}, @@ -439,6 +504,11 @@ namespace Umbraco.Web.Editors var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); return userIdentity; } + + private IAuthenticationManager AuthenticationManager + { + get { return OwinContext.Authentication; } + } /// diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs index f32a379218..0d9d859b3d 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs @@ -42,5 +42,8 @@ namespace Umbraco.Web.Models.ContentEditing /// [DataMember(Name = "allowedSections")] public IEnumerable AllowedSections { get; set; } + + [DataMember(Name = "linkedLogins")] + public IEnumerable> LinkedLogins { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/UnLinkLoginModel.cs b/src/Umbraco.Web/Models/UnLinkLoginModel.cs new file mode 100644 index 0000000000..776baf03fc --- /dev/null +++ b/src/Umbraco.Web/Models/UnLinkLoginModel.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models +{ + public class UnLinkLoginModel + { + [Required] + [DataMember(Name = "loginProvider", IsRequired = true)] + public string LoginProvider { get; set; } + + [Required] + [DataMember(Name = "providerKey", IsRequired = true)] + public string ProviderKey { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Mvc/UmbracoController.cs b/src/Umbraco.Web/Mvc/UmbracoController.cs index 1ab13cedac..241d22b16e 100644 --- a/src/Umbraco.Web/Mvc/UmbracoController.cs +++ b/src/Umbraco.Web/Mvc/UmbracoController.cs @@ -1,5 +1,7 @@ using System; +using System.Web; using System.Web.Mvc; +using Microsoft.Owin; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Services; @@ -24,6 +26,11 @@ namespace Umbraco.Web.Mvc } + protected IOwinContext OwinContext + { + get { return Request.GetOwinContext(); } + } + private UmbracoHelper _umbraco; /// diff --git a/src/Umbraco.Web/Security/Identity/AuthenticationManagerExtensions.cs b/src/Umbraco.Web/Security/Identity/AuthenticationManagerExtensions.cs index 52f50418e9..0acd002d42 100644 --- a/src/Umbraco.Web/Security/Identity/AuthenticationManagerExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AuthenticationManagerExtensions.cs @@ -36,6 +36,38 @@ namespace Umbraco.Web.Security.Identity }; } + /// + /// Extracts login info out of an external identity + /// + /// + /// + /// key that will be used to find the userId to verify + /// + /// the value expected to be found using the xsrfKey in the AuthenticationResult.Properties + /// dictionary + /// + /// + public static async Task GetExternalLoginInfoAsync(this IAuthenticationManager manager, + string authenticationType, + string xsrfKey, string expectedValue) + { + if (manager == null) + { + throw new ArgumentNullException("manager"); + } + var result = await manager.AuthenticateAsync(authenticationType); + // Verify that the userId is the same as what we expect if requested + if (result != null && + result.Properties != null && + result.Properties.Dictionary != null && + result.Properties.Dictionary.ContainsKey(xsrfKey) && + result.Properties.Dictionary[xsrfKey] == expectedValue) + { + return GetExternalLoginInfo(result); + } + return null; + } + /// /// Extracts login info out of an external identity /// diff --git a/src/Umbraco.Web/UI/JavaScript/Main.js b/src/Umbraco.Web/UI/JavaScript/Main.js index 520620af7d..afc4706ca3 100644 --- a/src/Umbraco.Web/UI/JavaScript/Main.js +++ b/src/Umbraco.Web/UI/JavaScript/Main.js @@ -3,6 +3,8 @@ LazyLoad.js("##JsInitialize##", function () { UmbClientMgr.setUmbracoPath('"##UmbracoPath##"'); jQuery(document).ready(function () { + angular.bootstrap(document, ['umbraco']); + }); }); \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index f40dc7bbd1..1edac1acb3 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -316,6 +316,7 @@ + diff --git a/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs b/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs index 8eeada541e..bd2290ce17 100644 --- a/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs +++ b/src/Umbraco.Web/WebApi/HttpRequestMessageExtensions.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using System.Web; using System.Web.Http; using System.Web.Http.ModelBinding; +using Microsoft.Owin; using Umbraco.Core; namespace Umbraco.Web.WebApi @@ -16,6 +17,20 @@ namespace Umbraco.Web.WebApi public static class HttpRequestMessageExtensions { + + /// + /// Borrowed from the latest Microsoft.AspNet.WebApi.Owin package which we cannot use because of a later webapi dependency + /// + /// + /// + internal static Attempt TryGetOwinContext(this HttpRequestMessage request) + { + var httpContext = request.TryGetHttpContext(); + return httpContext + ? Attempt.Succeed(httpContext.Result.GetOwinContext()) + : Attempt.Fail(); + } + /// /// Tries to retrieve the current HttpContext if one exists. /// diff --git a/src/Umbraco.Web/WebApi/UmbracoApiController.cs b/src/Umbraco.Web/WebApi/UmbracoApiController.cs index 9b1a37f4f7..33df591015 100644 --- a/src/Umbraco.Web/WebApi/UmbracoApiController.cs +++ b/src/Umbraco.Web/WebApi/UmbracoApiController.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Web; using System.Web.Http; using System.Web.Http.ModelBinding; +using Microsoft.Owin; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -42,6 +43,15 @@ namespace Umbraco.Web.WebApi return Request.TryGetHttpContext(); } + /// + /// Tries to retrieve the current HttpContext if one exists. + /// + /// + protected Attempt TryGetOwinContext() + { + return Request.TryGetOwinContext(); + } + /// /// Returns an ILogger /// From a2a8c8fbd72603935d26edf31678aea149f7f98b Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Feb 2015 17:26:28 +0100 Subject: [PATCH 10/38] updated to latest owin --- src/SQLCE4Umbraco/app.config | 16 +++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 11 +++++---- src/Umbraco.Core/app.config | 16 +++++++++++++ src/Umbraco.Core/packages.config | 4 ++-- src/Umbraco.Tests/App.config | 24 +++++++++++++++++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 12 +++++----- src/Umbraco.Web.UI/packages.config | 4 ++-- src/Umbraco.Web/Umbraco.Web.csproj | 8 +++---- src/Umbraco.Web/app.config | 12 ++++++++++ src/Umbraco.Web/packages.config | 4 ++-- src/UmbracoExamine/app.config | 16 +++++++++++++ src/umbraco.MacroEngines/app.config | 12 ++++++++++ src/umbraco.businesslogic/app.config | 16 +++++++++++++ src/umbraco.businesslogic/packages.config | 2 +- .../umbraco.businesslogic.csproj | 5 ++-- src/umbraco.cms/app.config | 16 +++++++++++++ src/umbraco.controls/app.config | 16 +++++++++++++ src/umbraco.datalayer/app.config | 12 ++++++++++ src/umbraco.editorControls/app.config | 12 ++++++++++ src/umbraco.macroRenderings/app.config | 16 +++++++++++++ src/umbraco.providers/app.config | 12 ++++++++++ 21 files changed, 222 insertions(+), 24 deletions(-) diff --git a/src/SQLCE4Umbraco/app.config b/src/SQLCE4Umbraco/app.config index 8f828418f3..777017ce39 100644 --- a/src/SQLCE4Umbraco/app.config +++ b/src/SQLCE4Umbraco/app.config @@ -14,6 +14,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index bd32b08677..85fe6d55bc 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -60,12 +60,13 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll - - ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll - - + False - ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + + + ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll + True False diff --git a/src/Umbraco.Core/app.config b/src/Umbraco.Core/app.config index 8f828418f3..777017ce39 100644 --- a/src/Umbraco.Core/app.config +++ b/src/Umbraco.Core/app.config @@ -14,6 +14,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index e8702a60bc..fd2be29ee3 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -12,8 +12,8 @@ - - + + diff --git a/src/Umbraco.Tests/App.config b/src/Umbraco.Tests/App.config index 86e6a0cbfe..893dd3c33f 100644 --- a/src/Umbraco.Tests/App.config +++ b/src/Umbraco.Tests/App.config @@ -156,6 +156,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index cbc8c23880..f84cdc7f82 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -158,17 +158,17 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll - - False - ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + True False ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - - ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll - True + + False + ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll False diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index c55244e7bf..f60d068561 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -23,9 +23,9 @@ - + - + diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 1edac1acb3..d409c220ec 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -138,16 +138,16 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll - + False - ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll - + False - ..\packages\Microsoft.Owin.Security.3.0.0\lib\net45\Microsoft.Owin.Security.dll + ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll False diff --git a/src/Umbraco.Web/app.config b/src/Umbraco.Web/app.config index 051211ebf7..fdd47d8fc1 100644 --- a/src/Umbraco.Web/app.config +++ b/src/Umbraco.Web/app.config @@ -53,6 +53,18 @@ + + + + + + + + + + + + diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 1429ad68d5..318ccab708 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -19,9 +19,9 @@ - + - + diff --git a/src/UmbracoExamine/app.config b/src/UmbracoExamine/app.config index b77bae14a4..e25336af02 100644 --- a/src/UmbracoExamine/app.config +++ b/src/UmbracoExamine/app.config @@ -14,6 +14,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/umbraco.MacroEngines/app.config b/src/umbraco.MacroEngines/app.config index a3f8757270..9176f3e6f5 100644 --- a/src/umbraco.MacroEngines/app.config +++ b/src/umbraco.MacroEngines/app.config @@ -28,6 +28,18 @@ + + + + + + + + + + + + diff --git a/src/umbraco.businesslogic/app.config b/src/umbraco.businesslogic/app.config index b77bae14a4..e25336af02 100644 --- a/src/umbraco.businesslogic/app.config +++ b/src/umbraco.businesslogic/app.config @@ -14,6 +14,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/umbraco.businesslogic/packages.config b/src/umbraco.businesslogic/packages.config index 8ae1655c0d..b1c4140b1a 100644 --- a/src/umbraco.businesslogic/packages.config +++ b/src/umbraco.businesslogic/packages.config @@ -4,7 +4,7 @@ - + \ No newline at end of file diff --git a/src/umbraco.businesslogic/umbraco.businesslogic.csproj b/src/umbraco.businesslogic/umbraco.businesslogic.csproj index 7accd2ccb5..fdc3b405ad 100644 --- a/src/umbraco.businesslogic/umbraco.businesslogic.csproj +++ b/src/umbraco.businesslogic/umbraco.businesslogic.csproj @@ -113,8 +113,9 @@ ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll - - ..\packages\Microsoft.Owin.3.0.0\lib\net45\Microsoft.Owin.dll + + False + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll True diff --git a/src/umbraco.cms/app.config b/src/umbraco.cms/app.config index b77bae14a4..e25336af02 100644 --- a/src/umbraco.cms/app.config +++ b/src/umbraco.cms/app.config @@ -14,6 +14,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/umbraco.controls/app.config b/src/umbraco.controls/app.config index b77bae14a4..e25336af02 100644 --- a/src/umbraco.controls/app.config +++ b/src/umbraco.controls/app.config @@ -14,6 +14,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/umbraco.datalayer/app.config b/src/umbraco.datalayer/app.config index 6acdd4b4ca..777017ce39 100644 --- a/src/umbraco.datalayer/app.config +++ b/src/umbraco.datalayer/app.config @@ -16,6 +16,18 @@ + + + + + + + + + + + + diff --git a/src/umbraco.editorControls/app.config b/src/umbraco.editorControls/app.config index 743b7c93ca..38ef813341 100644 --- a/src/umbraco.editorControls/app.config +++ b/src/umbraco.editorControls/app.config @@ -33,6 +33,18 @@ + + + + + + + + + + + + diff --git a/src/umbraco.macroRenderings/app.config b/src/umbraco.macroRenderings/app.config index e72c720717..ed1446828c 100644 --- a/src/umbraco.macroRenderings/app.config +++ b/src/umbraco.macroRenderings/app.config @@ -10,6 +10,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/umbraco.providers/app.config b/src/umbraco.providers/app.config index 267e89a2dd..e25336af02 100644 --- a/src/umbraco.providers/app.config +++ b/src/umbraco.providers/app.config @@ -16,6 +16,18 @@ + + + + + + + + + + + + From afa4c7b697f6e5e98f99da9879350be95b8bd3a3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Feb 2015 18:51:44 +0100 Subject: [PATCH 11/38] open id connect is working with azure ad --- src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 135 ++++++++++++++++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 18 +++ src/Umbraco.Web.UI/packages.config | 4 + .../Editors/BackOfficeController.cs | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 6 + src/Umbraco.Web/packages.config | 1 + 6 files changed, 153 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index a11e071aa3..a4eb25b341 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -1,14 +1,11 @@ using System; -using System.Collections.Generic; -using System.Linq; +using System.Globalization; +using System.Net.Http; +using System.Threading.Tasks; using System.Web; -using System.Web.Security; -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; +using Microsoft.IdentityModel.Clients.ActiveDirectory; using Microsoft.Owin; -using Microsoft.Owin.Security.Cookies; -using Microsoft.Owin.Security.Google; -using Umbraco.Web.Security.Identity; +using Microsoft.Owin.Security.OpenIdConnect; using Owin; using Umbraco.Core; using Umbraco.Core.Security; @@ -26,8 +23,23 @@ namespace Umbraco.Web.UI public class OwinStartup { + public async Task DoStuff() + { + var client = new HttpClient(); + + using (var request = await client.PostAsJsonAsync("", "123")) + { + + } + } + public void Configuration(IAppBuilder app) { + + + + + //Single method to configure the Identity user manager for use with Umbraco app.ConfigureUserManagerForUmbracoBackOffice( ApplicationContext.Current, @@ -63,12 +75,111 @@ namespace Umbraco.Web.UI //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); - app.UseGoogleAuthentication( - clientId: "1072120697051-07jlhgrd5hodsfe7dgqimdie8qc1omet.apps.googleusercontent.com", - clientSecret: "Ue9swN0lEX9rwxzQz1Y_tFzg"); - + + var authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant); + app.UseOpenIdConnectAuthentication( + new OpenIdConnectAuthenticationOptions + { + ClientId = clientId, + Authority = authority, + PostLogoutRedirectUri = postLoginRedirectUri, + + Notifications = new OpenIdConnectAuthenticationNotifications() + { + // + // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. + // + AuthorizationCodeReceived = (context) => + { + var code = context.Code; + + var credential = new ClientCredential(clientId, appKey); + var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; + var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); + AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( + code, + //new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), + new Uri( + HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + + HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), + credential, + graphResourceId); + + return Task.FromResult(0); + } + + } + + }); + } } + + public class NaiveSessionCache : TokenCache + { + private static readonly object FileLock = new object(); + string UserObjectId = string.Empty; + string CacheId = string.Empty; + public NaiveSessionCache(string userId) + { + UserObjectId = userId; + CacheId = UserObjectId + "_TokenCache"; + + this.AfterAccess = AfterAccessNotification; + this.BeforeAccess = BeforeAccessNotification; + Load(); + } + + public void Load() + { + lock (FileLock) + { + this.Deserialize((byte[])HttpContext.Current.Session[CacheId]); + } + } + + public void Persist() + { + lock (FileLock) + { + // reflect changes in the persistent store + HttpContext.Current.Session[CacheId] = this.Serialize(); + // once the write operation took place, restore the HasStateChanged bit to false + this.HasStateChanged = false; + } + } + + // Empties the persistent store. + public override void Clear() + { + base.Clear(); + System.Web.HttpContext.Current.Session.Remove(CacheId); + } + + public override void DeleteItem(TokenCacheItem item) + { + base.DeleteItem(item); + Persist(); + } + + // Triggered right before ADAL needs to access the cache. + // Reload the cache from the persistent store in case it changed since the last access. + void BeforeAccessNotification(TokenCacheNotificationArgs args) + { + Load(); + } + + // Triggered right after ADAL accessed the cache. + void AfterAccessNotification(TokenCacheNotificationArgs args) + { + // if the access operation resulted in a cache update + if (this.HasStateChanged) + { + Persist(); + } + } + } + } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index f84cdc7f82..52287c9084 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -158,6 +158,16 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll + + + ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll + + + False + ..\packages\Microsoft.IdentityModel.Protocol.Extensions.1.0.1\lib\net45\Microsoft.IdentityModel.Protocol.Extensions.dll + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll True @@ -181,6 +191,9 @@ False ..\packages\Microsoft.Owin.Security.OAuth.3.0.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + + ..\packages\Microsoft.Owin.Security.OpenIdConnect.3.0.1\lib\net45\Microsoft.Owin.Security.OpenIdConnect.dll + ..\packages\Microsoft.Bcl.Async.1.0.165\lib\net45\Microsoft.Threading.Tasks.dll @@ -232,6 +245,11 @@ + + + False + ..\packages\System.IdentityModel.Tokens.Jwt.4.0.1\lib\net45\System.IdentityModel.Tokens.Jwt.dll + False diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index f60d068561..d7e4304b96 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -22,6 +22,8 @@ + + @@ -29,6 +31,7 @@ + @@ -36,5 +39,6 @@ + \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 47866de5c9..336e1dbfde 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -687,7 +687,7 @@ namespace Umbraco.Web.Editors //Ensure the forms auth module doesn't do a redirect! context.HttpContext.Response.SuppressFormsAuthenticationRedirect = true; - var properties = new AuthenticationProperties() { RedirectUri = RedirectUri }; + var properties = new AuthenticationProperties() { RedirectUri = RedirectUri.EnsureEndsWith('/') }; if (UserId != null) { properties.Dictionary[XsrfKey] = UserId; diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index d409c220ec..0117715330 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -138,6 +138,12 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll + + + ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll + False ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 318ccab708..6f8f81ffbc 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -18,6 +18,7 @@ + From d9cf9cee88dbf15e1657241210ae3c7a1a76b6fc Mon Sep 17 00:00:00 2001 From: Shannon Date: Sun, 22 Feb 2015 13:29:00 +0100 Subject: [PATCH 12/38] Includes nice social buttons, updates styling on login and user panel, updates logic to un-link accounts --- src/Umbraco.Web.UI.Client/bower.json | 12 ++- .../src/common/resources/auth.resource.js | 10 ++ src/Umbraco.Web.UI.Client/src/less/panel.less | 9 +- .../src/views/common/dialogs/login.html | 9 +- .../views/common/dialogs/user.controller.js | 57 +++++----- .../src/views/common/dialogs/user.html | 31 +++--- src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 100 +++++++++++------- .../umbraco/Views/Default.cshtml | 9 +- .../Editors/AuthenticationController.cs | 16 +-- .../Models/ContentEditing/UserDetail.cs | 3 +- 10 files changed, 165 insertions(+), 91 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index e8ad94653f..ce312d669a 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -19,6 +19,7 @@ "typeahead.js": "~0.10.5", "underscore": "~1.7.0", "rgrove-lazyload": "*", + "bootstrap-social": "~4.8.0" "jquery": "2.0.3", "jquery-file-upload": "~9.4.0", "jquery-ui": "1.10.3", @@ -34,8 +35,15 @@ "underscore": { "": "underscore-min.{js,map}" }, - "angular-dynamic-locale": { - "": "tmhDynamicLocale.min.{js,js.map}" + "bootstrap-social": { + "": "bootstrap-social.css" + }, + "font-awesome": { + "css": "css/font-awesome.min.css", + "fonts" : "fonts/*" + }, + "bootstrap": { + "ignore": "*.ignore" }, "jquery": { "": "jquery.min.{js,map}" diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js index 973b841cdd..f32602bda6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/auth.resource.js @@ -123,6 +123,16 @@ function authResource($q, $http, umbRequestHelper, angularHelper) { "GetCurrentUser")), 'Server call failed for getting current user'); }, + + getCurrentUserLinkedLogins: function () { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "authenticationApiBaseUrl", + "GetCurrentUserLinkedLogins")), + 'Server call failed for getting current users linked logins'); + }, /** * @ngdoc method diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 7fc2d061ea..62a5d21ac7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -280,4 +280,11 @@ .umb-dialog a.text-success:hover, .umb-dialog a.text-success:focus, .umb-panel a.text-success:hover, -.umb-panel a.text-success:focus { color: darken(@formSuccessText, 10%); } \ No newline at end of file +.umb-panel a.text-success:focus { color: darken(@formSuccessText, 10%); } + +.umb-user-panel .external-logins form { + margin:0; +} +.umb-user-panel .external-logins button { + margin:5px; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index 4981b4ca12..dd7971b678 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -36,11 +36,14 @@
- - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js index c6531e6016..6048edfa95 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.controller.js @@ -51,7 +51,37 @@ angular.module("umbraco") }, 1000, false); // 1 second, do NOT execute a global digest } - $scope.unlink = function(e, loginProvider, providerKey) { + function updateUserInfo() { + //get the user + userService.getCurrentUser().then(function (user) { + $scope.user = user; + if ($scope.user) { + $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; + $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; + //set the timer + updateTimeout(); + + authResource.getCurrentUserLinkedLogins().then(function(logins) { + //reset all to be un-linked + for (var provider in $scope.externalLoginProviders) { + $scope.externalLoginProviders[provider].linkedProviderKey = undefined; + } + + //set the linked logins + for (var login in logins) { + var found = _.find($scope.externalLoginProviders, function (i) { + return i.authType == login; + }); + if (found) { + found.linkedProviderKey = logins[login]; + } + } + }); + } + }); + } + + $scope.unlink = function (e, loginProvider, providerKey) { var result = confirm("Are you sure you want to unlink this account?"); if (!result) { e.preventDefault(); @@ -59,32 +89,11 @@ angular.module("umbraco") } authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { - var asdf = ";" - }, function(err) { - var asdf = err; + updateUserInfo(); }); } - //get the user - userService.getCurrentUser().then(function (user) { - $scope.user = user; - if ($scope.user) { - $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; - $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; - //set the timer - updateTimeout(); - - //set the linked logins - for (var login in $scope.user.linkedLogins) { - var found = _.find($scope.externalLoginProviders, function(i) { - return i.authType == login; - }); - if (found) { - found.linkedProviderKey = $scope.user.linkedLogins[login]; - } - } - } - }); + updateUserInfo(); //remove all event handlers $scope.$on('$destroy', function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html index c73de2bb3a..37751bfab6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html @@ -1,4 +1,4 @@ -
+
@@ -30,30 +30,35 @@

-
+
External login providers
-
+
-
- + value="{{login.authType}}"> + + Link your {{login.caption}} account +
- - {{login.caption}} - + value="{{login.authType}}"> + + Un-link your {{login.caption}} account +
diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index a4eb25b341..930aed24b0 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Web; using Microsoft.IdentityModel.Clients.ActiveDirectory; using Microsoft.Owin; +using Microsoft.Owin.Security.Google; using Microsoft.Owin.Security.OpenIdConnect; using Owin; using Umbraco.Core; @@ -73,59 +74,84 @@ namespace Umbraco.Web.UI .UseUmbracoBackOfficeExternalCookieAuthentication(); //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); - - + //app.UseGoogleAuthentication( + // clientId: "1072120697051-07jlhgrd5hodsfe7dgqimdie8qc1omet.apps.googleusercontent.com", + // clientSecret: "Ue9swN0lEX9rwxzQz1Y_tFzg"); + + var googleOptions = new GoogleOAuth2AuthenticationOptions + { + + }; + googleOptions.Description.Properties["SocialStyle"] = "btn-google-plus"; + googleOptions.Description.Properties["SocialIcon"] = "fa-google-plus"; + googleOptions.Caption = "Google"; + app.UseGoogleAuthentication(googleOptions); + + //AD docs are here: + // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet + var authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant); - app.UseOpenIdConnectAuthentication( - new OpenIdConnectAuthenticationOptions + var adOptions = new OpenIdConnectAuthenticationOptions + { + //NOTE: This by default is 'OpenIdConnect' but that doesn't match what identity actually stores in the + // loginProvider field in the database which is something like: https://sts.windows.net/1234.... + // which is something based on your AD setup. This value needs to match in order for accounts to detected as linked/un-linked + // in the back office. + AuthenticationType = "https://sts.windows.net/3bb0b4c5-364f-4394-ad36-0f29f95e5ddd/", + + ClientId = clientId, + Authority = authority, + PostLogoutRedirectUri = postLoginRedirectUri, + Notifications = new OpenIdConnectAuthenticationNotifications() { - ClientId = clientId, - Authority = authority, - PostLogoutRedirectUri = postLoginRedirectUri, - - Notifications = new OpenIdConnectAuthenticationNotifications() + // + // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. + // + AuthorizationCodeReceived = (context) => { - // - // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. - // - AuthorizationCodeReceived = (context) => - { - var code = context.Code; + var code = context.Code; - var credential = new ClientCredential(clientId, appKey); - var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; - var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); - AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( - code, - //new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), - new Uri( - HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + - HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), - credential, - graphResourceId); - - return Task.FromResult(0); - } + var credential = new ClientCredential(clientId, appKey); + var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; + var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); + AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( + code, + //NOTE: This URL needs to match EXACTLY the same path that is configured in the AD + // configuration. + new Uri( + HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + + HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), + credential, + graphResourceId); + return Task.FromResult(0); } - }); + } + + }; + adOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; + adOptions.Description.Properties["SocialIcon"] = "fa-windows"; + adOptions.Caption = "Active Directory"; + app.UseOpenIdConnectAuthentication(adOptions); } } + //NOTE: Not sure exactly what this is for but it is found in the AD source demo: + // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs public class NaiveSessionCache : TokenCache { private static readonly object FileLock = new object(); - string UserObjectId = string.Empty; - string CacheId = string.Empty; + readonly string _userObjectId = string.Empty; + readonly string _cacheId = string.Empty; public NaiveSessionCache(string userId) { - UserObjectId = userId; - CacheId = UserObjectId + "_TokenCache"; + _userObjectId = userId; + _cacheId = _userObjectId + "_TokenCache"; this.AfterAccess = AfterAccessNotification; this.BeforeAccess = BeforeAccessNotification; @@ -136,7 +162,7 @@ namespace Umbraco.Web.UI { lock (FileLock) { - this.Deserialize((byte[])HttpContext.Current.Session[CacheId]); + this.Deserialize((byte[])HttpContext.Current.Session[_cacheId]); } } @@ -145,7 +171,7 @@ namespace Umbraco.Web.UI lock (FileLock) { // reflect changes in the persistent store - HttpContext.Current.Session[CacheId] = this.Serialize(); + HttpContext.Current.Session[_cacheId] = this.Serialize(); // once the write operation took place, restore the HasStateChanged bit to false this.HasStateChanged = false; } @@ -155,7 +181,7 @@ namespace Umbraco.Web.UI public override void Clear() { base.Clear(); - System.Web.HttpContext.Current.Session.Remove(CacheId); + System.Web.HttpContext.Current.Session.Remove(_cacheId); } public override void DeleteItem(TokenCacheItem item) diff --git a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml index c51e1c6ab9..a392942fe1 100644 --- a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml @@ -28,8 +28,13 @@ Umbraco - @{ Html.RequiresCss("assets/css/umbraco.css", "Umbraco");} - @{ Html.RequiresCss("tree/treeicons.css", "UmbracoClient");} + @{ + Html + .RequiresCss("assets/css/umbraco.css", "Umbraco") + .RequiresCss("tree/treeicons.css", "UmbracoClient") + .RequiresCss("lib/bootstrap-social/bootstrap-social.css", "Umbraco") + .RequiresCss("lib/font-awesome/css/font-awesome.min.css", "Umbraco"); + } @Html.RenderCssHere( new BasicPath("Umbraco", IOHelper.ResolveUrl(SystemDirectories.Umbraco)), new BasicPath("UmbracoClient", IOHelper.ResolveUrl(SystemDirectories.UmbracoClient))) diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index f6fcb2c0e7..a1e12401b1 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -139,7 +139,7 @@ namespace Umbraco.Web.Editors /// [WebApi.UmbracoAuthorize] [SetAngularAntiForgeryTokens] - public async Task GetCurrentUser() + public UserDetail GetCurrentUser() { var user = Services.UserService.GetUserById(UmbracoContext.Security.GetUserId()); var result = Mapper.Map(user); @@ -150,15 +150,17 @@ namespace Umbraco.Web.Editors result.SecondsUntilTimeout = httpContextAttempt.Result.GetRemainingAuthSeconds(); } - //now we need to fill in the user's linked logins, we can't do this in the mapper because it has no access to the - // user manager - - var identityUser = await UserManager.FindByIdAsync(user.Id); - result.LinkedLogins = identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); - return result; } + [WebApi.UmbracoAuthorize] + [SetAngularAntiForgeryTokens] + public async Task> GetCurrentUserLinkedLogins() + { + var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId()); + return identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); + } + private BackOfficeUserManager _userManager; protected BackOfficeUserManager UserManager diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs index 0d9d859b3d..d27736576f 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs @@ -43,7 +43,6 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "allowedSections")] public IEnumerable AllowedSections { get; set; } - [DataMember(Name = "linkedLogins")] - public IEnumerable> LinkedLogins { get; set; } + } } \ No newline at end of file From 4b156ba27e939003f076e818423021faebcb60de Mon Sep 17 00:00:00 2001 From: Shannon Date: Sun, 22 Feb 2015 15:10:14 +0100 Subject: [PATCH 13/38] Starts stubbing out role manager code --- .../Models/Identity/BackOfficeIdentityRole.cs | 22 ++++++ .../Security/BackOfficeRoleManager.cs | 27 ++++++++ .../Security/BackOfficeRoleStore.cs | 67 +++++++++++++++++++ .../Security/BackOfficeUserManager.cs | 37 +++------- .../Security/BackOfficeUserStore.cs | 18 ++++- ...> IUmbracoMemberTypeMembershipProvider.cs} | 0 .../Security/MembershipPasswordHasher.cs | 31 +++++++++ src/Umbraco.Core/Services/IUserService.cs | 1 + src/Umbraco.Core/Umbraco.Core.csproj | 6 +- .../Security/Identity/AppBuilderExtensions.cs | 18 ++--- 10 files changed, 188 insertions(+), 39 deletions(-) create mode 100644 src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs create mode 100644 src/Umbraco.Core/Security/BackOfficeRoleManager.cs create mode 100644 src/Umbraco.Core/Security/BackOfficeRoleStore.cs rename src/Umbraco.Core/Security/{UmbracoMembersMembershipProviderBase.cs => IUmbracoMemberTypeMembershipProvider.cs} (100%) create mode 100644 src/Umbraco.Core/Security/MembershipPasswordHasher.cs diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs new file mode 100644 index 0000000000..9fb4c51f52 --- /dev/null +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Models.Identity +{ + public class BackOfficeIdentityRole : IRole + { + public BackOfficeIdentityRole(string id) + { + Id = id; + } + + /// + /// Id of the role + /// + public string Id { get; private set; } + + /// + /// Name of the role + /// + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeRoleManager.cs b/src/Umbraco.Core/Security/BackOfficeRoleManager.cs new file mode 100644 index 0000000000..c49a6b943b --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeRoleManager.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + public class BackOfficeRoleManager : RoleManager + { + /// + /// Constructor + /// + /// The IRoleStore is responsible for commiting changes via the UpdateAsync/CreateAsync methods + public BackOfficeRoleManager(IRoleStore store) : base(store) + { + } + + public static BackOfficeRoleManager Create( + IdentityFactoryOptions options) + { + //TODO: Set this up! + + var manager = new BackOfficeRoleManager(new BackOfficeRoleStore()); + + return manager; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeRoleStore.cs b/src/Umbraco.Core/Security/BackOfficeRoleStore.cs new file mode 100644 index 0000000000..603f2e1ed2 --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeRoleStore.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + public class BackOfficeRoleStore : DisposableObject, IRoleStore + { + /// + /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. + /// + protected override void DisposeResources() + { + } + + /// + /// Create a new role + /// + /// + /// + public Task CreateAsync(BackOfficeIdentityRole role) + { + throw new NotImplementedException(); + } + + /// + /// Update a role + /// + /// + /// + public Task UpdateAsync(BackOfficeIdentityRole role) + { + throw new NotImplementedException(); + } + + /// + /// Delete a role + /// + /// + /// + public Task DeleteAsync(BackOfficeIdentityRole role) + { + throw new NotImplementedException(); + } + + /// + /// Find a role by id + /// + /// + /// + public Task FindByIdAsync(string roleId) + { + throw new NotImplementedException(); + } + + /// + /// Find a role by name + /// + /// + /// + public Task FindByNameAsync(string roleName) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 9634b17c73..b65b9dfaa1 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -12,33 +12,6 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Security { - /// - /// A custom password hasher that conforms to the current password hashing done in Umbraco - /// - internal class MembershipPasswordHasher : IPasswordHasher - { - private readonly MembershipProviderBase _provider; - - public MembershipPasswordHasher(MembershipProviderBase provider) - { - _provider = provider; - } - - public string HashPassword(string password) - { - return _provider.HashPasswordForStorage(password); - } - - public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) - { - return _provider.VerifyPassword(providedPassword, hashedPassword) - ? PasswordVerificationResult.Success - : PasswordVerificationResult.Failed; - } - - - } - /// /// Back office user manager /// @@ -51,6 +24,12 @@ namespace Umbraco.Core.Security #region What we support currently + //NOTE: Not sure if we really want/need to ever support this + public override bool SupportsUserClaim + { + get { return false; } + } + //TODO: Support this public override bool SupportsUserRole { @@ -75,11 +54,13 @@ namespace Umbraco.Core.Security get { return false; } } + //TODO: Support this public override bool SupportsUserTwoFactor { get { return false; } } + //TODO: Support this public override bool SupportsUserPhoneNumber { get { return false; } @@ -127,7 +108,7 @@ namespace Umbraco.Core.Security } //custom identity factory for creating the identity object for which we auth against in the back office - manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); + manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); //NOTE: Not implementing these currently diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 83676b9c1f..6dd4aadc3e 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -11,7 +11,23 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Security { - public class BackOfficeUserStore : DisposableObject, IUserStore, IUserPasswordStore, IUserEmailStore, IUserLoginStore + public class BackOfficeUserStore : DisposableObject, + IUserStore, + IUserPasswordStore, + IUserEmailStore, + IUserLoginStore + + //IUserRoleStore, + + //TODO: This will require additional columns/tables + //IUserLockoutStore + + //TODO: Implement this - might need to add a new column for this + // http://stackoverflow.com/questions/19487322/what-is-asp-net-identitys-iusersecuritystampstoretuser-interface + //IUserSecurityStampStore + + //TODO: To do this we need to implement IQueryable - seems pretty overkill? + //IQueryableUserStore { private readonly IUserService _userService; private readonly IExternalLoginService _externalLoginService; diff --git a/src/Umbraco.Core/Security/UmbracoMembersMembershipProviderBase.cs b/src/Umbraco.Core/Security/IUmbracoMemberTypeMembershipProvider.cs similarity index 100% rename from src/Umbraco.Core/Security/UmbracoMembersMembershipProviderBase.cs rename to src/Umbraco.Core/Security/IUmbracoMemberTypeMembershipProvider.cs diff --git a/src/Umbraco.Core/Security/MembershipPasswordHasher.cs b/src/Umbraco.Core/Security/MembershipPasswordHasher.cs new file mode 100644 index 0000000000..56daa3efdd --- /dev/null +++ b/src/Umbraco.Core/Security/MembershipPasswordHasher.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNet.Identity; + +namespace Umbraco.Core.Security +{ + /// + /// A custom password hasher that conforms to the current password hashing done in Umbraco + /// + internal class MembershipPasswordHasher : IPasswordHasher + { + private readonly MembershipProviderBase _provider; + + public MembershipPasswordHasher(MembershipProviderBase provider) + { + _provider = provider; + } + + public string HashPassword(string password) + { + return _provider.HashPasswordForStorage(password); + } + + public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword) + { + return _provider.VerifyPassword(providedPassword, hashedPassword) + ? PasswordVerificationResult.Success + : PasswordVerificationResult.Failed; + } + + + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IUserService.cs b/src/Umbraco.Core/Services/IUserService.cs index 5a77956931..aa8250cf59 100644 --- a/src/Umbraco.Core/Services/IUserService.cs +++ b/src/Umbraco.Core/Services/IUserService.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Services diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 85fe6d55bc..0c2c0bee73 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -345,6 +345,7 @@ + @@ -419,8 +420,11 @@ + + + @@ -1144,7 +1148,7 @@ - + diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index c1a6ed26a8..0e8712ed07 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -37,6 +37,9 @@ namespace Umbraco.Web.Security.Identity appContext.Services.UserService, appContext.Services.ExternalLoginService, userMembershipProvider)); + + //Configure Umbraco role manager to be created per request + app.CreatePerOwinContext((options, owinContext) => BackOfficeRoleManager.Create(options)); } /// @@ -70,14 +73,16 @@ namespace Umbraco.Web.Security.Identity return app; } + /// + /// 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) { if (app == null) throw new ArgumentNullException("app"); - //TODO: Figure out why this isn't working and is only working with the default one, must be a reference somewhere - - //app.UseExternalSignInCookie("UmbracoExternalCookie"); - app.SetDefaultSignInAsAuthenticationType("UmbracoExternalCookie"); app.UseCookieAuthentication(new CookieAuthenticationOptions { @@ -93,11 +98,6 @@ namespace Umbraco.Web.Security.Identity CookieDomain = UmbracoConfig.For.UmbracoSettings().Security.AuthCookieDomain }); - - //NOTE: This works, but this is just the default implementation which we don't want because other devs - //might want to use this... right? - //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); - return app; } From ff602da0fa3f230982e00ff3d8edb677c3ee94a7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 5 Mar 2015 14:02:39 +1100 Subject: [PATCH 14/38] Updates owin packages and updates the web.config tempate for owin redirects --- src/SQLCE4Umbraco/SqlCE4Umbraco.csproj | 5 +++-- src/SQLCE4Umbraco/app.config | 4 ++-- src/Umbraco.Core/Umbraco.Core.csproj | 8 ++++---- src/Umbraco.Core/app.config | 4 ++-- src/Umbraco.Core/packages.config | 4 ++-- src/Umbraco.Tests/App.config | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 22 +++++++++++++--------- src/Umbraco.Web.UI/packages.config | 9 +++++---- src/Umbraco.Web.UI/web.Template.config | 17 +++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 13 +++++++------ src/Umbraco.Web/app.config | 4 ++-- src/Umbraco.Web/packages.config | 6 +++--- src/UmbracoExamine/app.config | 4 ++-- src/umbraco.MacroEngines/app.config | 4 ++-- src/umbraco.businesslogic/app.config | 4 ++-- src/umbraco.cms/app.config | 4 ++-- src/umbraco.controls/app.config | 4 ++-- src/umbraco.datalayer/app.config | 4 ++-- src/umbraco.editorControls/app.config | 4 ++-- src/umbraco.providers/app.config | 4 ++-- 20 files changed, 78 insertions(+), 54 deletions(-) diff --git a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj index b7d3f1f655..73983e7e30 100644 --- a/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj +++ b/src/SQLCE4Umbraco/SqlCE4Umbraco.csproj @@ -77,8 +77,9 @@ - - + + Designer + diff --git a/src/SQLCE4Umbraco/app.config b/src/SQLCE4Umbraco/app.config index 777017ce39..1f5a6442ad 100644 --- a/src/SQLCE4Umbraco/app.config +++ b/src/SQLCE4Umbraco/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 0c2c0bee73..52555fd9db 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -68,13 +68,13 @@ ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll True - + False - ..\packages\Microsoft.Owin.Security.Cookies.3.0.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll - + False - ..\packages\Microsoft.Owin.Security.OAuth.3.0.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll True diff --git a/src/Umbraco.Core/app.config b/src/Umbraco.Core/app.config index 777017ce39..1f5a6442ad 100644 --- a/src/Umbraco.Core/app.config +++ b/src/Umbraco.Core/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index fd2be29ee3..6281d1338f 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -14,8 +14,8 @@ - - + + diff --git a/src/Umbraco.Tests/App.config b/src/Umbraco.Tests/App.config index 893dd3c33f..f1917d16c0 100644 --- a/src/Umbraco.Tests/App.config +++ b/src/Umbraco.Tests/App.config @@ -164,7 +164,7 @@ - + @@ -180,7 +180,7 @@ - + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 52287c9084..1392750d0c 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -172,24 +172,28 @@ ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll True - + False - ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll False ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll - + False - ..\packages\Microsoft.Owin.Security.Cookies.3.0.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll - - ..\packages\Microsoft.Owin.Security.Google.3.0.0\lib\net45\Microsoft.Owin.Security.Google.dll + + ..\packages\Microsoft.Owin.Security.Facebook.3.0.1\lib\net45\Microsoft.Owin.Security.Facebook.dll - + False - ..\packages\Microsoft.Owin.Security.OAuth.3.0.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + ..\packages\Microsoft.Owin.Security.Google.3.0.1\lib\net45\Microsoft.Owin.Security.Google.dll + + + False + ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll ..\packages\Microsoft.Owin.Security.OpenIdConnect.3.0.1\lib\net45\Microsoft.Owin.Security.OpenIdConnect.dll @@ -2605,7 +2609,7 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True 7300 / - http://localhost:7301 + http://localhost:7300 False False diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index d7e4304b96..c0e0034b61 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -26,11 +26,12 @@ - + - - - + + + + diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 0d8400471f..b769cc7318 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -263,6 +263,23 @@ + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0117715330..537f7dd6b1 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -148,20 +148,21 @@ False ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll - - ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.0\lib\net45\Microsoft.Owin.Host.SystemWeb.dll + + False + ..\packages\Microsoft.Owin.Host.SystemWeb.3.0.1\lib\net45\Microsoft.Owin.Host.SystemWeb.dll False ..\packages\Microsoft.Owin.Security.3.0.1\lib\net45\Microsoft.Owin.Security.dll - + False - ..\packages\Microsoft.Owin.Security.Cookies.3.0.0\lib\net45\Microsoft.Owin.Security.Cookies.dll + ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll - + False - ..\packages\Microsoft.Owin.Security.OAuth.3.0.0\lib\net45\Microsoft.Owin.Security.OAuth.dll + ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll True diff --git a/src/Umbraco.Web/app.config b/src/Umbraco.Web/app.config index fdd47d8fc1..71898fd12e 100644 --- a/src/Umbraco.Web/app.config +++ b/src/Umbraco.Web/app.config @@ -57,7 +57,7 @@ - + @@ -65,7 +65,7 @@ - + diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 6f8f81ffbc..d1898c46fd 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -21,10 +21,10 @@ - + - - + + diff --git a/src/UmbracoExamine/app.config b/src/UmbracoExamine/app.config index e25336af02..4022c25600 100644 --- a/src/UmbracoExamine/app.config +++ b/src/UmbracoExamine/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/umbraco.MacroEngines/app.config b/src/umbraco.MacroEngines/app.config index 9176f3e6f5..cabc84546e 100644 --- a/src/umbraco.MacroEngines/app.config +++ b/src/umbraco.MacroEngines/app.config @@ -32,7 +32,7 @@ - + @@ -40,7 +40,7 @@ - + diff --git a/src/umbraco.businesslogic/app.config b/src/umbraco.businesslogic/app.config index e25336af02..4022c25600 100644 --- a/src/umbraco.businesslogic/app.config +++ b/src/umbraco.businesslogic/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/umbraco.cms/app.config b/src/umbraco.cms/app.config index e25336af02..4022c25600 100644 --- a/src/umbraco.cms/app.config +++ b/src/umbraco.cms/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/umbraco.controls/app.config b/src/umbraco.controls/app.config index e25336af02..4022c25600 100644 --- a/src/umbraco.controls/app.config +++ b/src/umbraco.controls/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/umbraco.datalayer/app.config b/src/umbraco.datalayer/app.config index 777017ce39..1f5a6442ad 100644 --- a/src/umbraco.datalayer/app.config +++ b/src/umbraco.datalayer/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/src/umbraco.editorControls/app.config b/src/umbraco.editorControls/app.config index 38ef813341..68046c3af5 100644 --- a/src/umbraco.editorControls/app.config +++ b/src/umbraco.editorControls/app.config @@ -37,7 +37,7 @@ - + @@ -45,7 +45,7 @@ - + diff --git a/src/umbraco.providers/app.config b/src/umbraco.providers/app.config index e25336af02..4022c25600 100644 --- a/src/umbraco.providers/app.config +++ b/src/umbraco.providers/app.config @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + From 7dc50fda260833d778ebee4817f1f7716b898d7c Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 20 Mar 2015 16:58:01 +1100 Subject: [PATCH 15/38] moves NaiveSessionCache to web proj --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 2 +- .../Security/Identity/NaiveSessionCache.cs | 84 +++++++++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 1392750d0c..4b8e33ca86 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2609,7 +2609,7 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True 7300 / - http://localhost:7300 + http://localhost:7301 False False diff --git a/src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs b/src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs new file mode 100644 index 0000000000..f4df1fbb30 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Microsoft.IdentityModel.Clients.ActiveDirectory; + +namespace Umbraco.Web.Security.Identity +{ + //NOTE: Not sure exactly what this is for but it is found in the AD source demo: + // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs + // apparently it is needed for AD auth, so we'll put it here for people to use. + // It would appear that this is better for whatever reason: https://github.com/OfficeDev/O365-WebApp-SingleTenant/blob/master/O365-WebApp-SingleTenant/Models/ADALTokenCache.cs + // and please note that that link came from finding this thread: https://twitter.com/chakkaradeep/status/544962341528285184 + + /// + /// This is required to initialize the AD Identity provider on startup + /// + public class NaiveSessionCache : TokenCache + { + private static readonly object FileLock = new object(); + readonly string _userObjectId = string.Empty; + readonly string _cacheId = string.Empty; + public NaiveSessionCache(string userId) + { + _userObjectId = userId; + _cacheId = _userObjectId + "_TokenCache"; + + this.AfterAccess = AfterAccessNotification; + this.BeforeAccess = BeforeAccessNotification; + Load(); + } + + public void Load() + { + lock (FileLock) + { + this.Deserialize((byte[])HttpContext.Current.Session[_cacheId]); + } + } + + public void Persist() + { + lock (FileLock) + { + // reflect changes in the persistent store + HttpContext.Current.Session[_cacheId] = this.Serialize(); + // once the write operation took place, restore the HasStateChanged bit to false + this.HasStateChanged = false; + } + } + + // Empties the persistent store. + public override void Clear() + { + base.Clear(); + System.Web.HttpContext.Current.Session.Remove(_cacheId); + } + + public override void DeleteItem(TokenCacheItem item) + { + base.DeleteItem(item); + Persist(); + } + + // Triggered right before ADAL needs to access the cache. + // Reload the cache from the persistent store in case it changed since the last access. + void BeforeAccessNotification(TokenCacheNotificationArgs args) + { + Load(); + } + + // Triggered right after ADAL accessed the cache. + void AfterAccessNotification(TokenCacheNotificationArgs args) + { + // if the access operation resulted in a cache update + if (this.HasStateChanged) + { + Persist(); + } + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 537f7dd6b1..79c59c85a4 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -560,6 +560,7 @@ + From 2d72a6687922ccc235f22571c93fbf0fd47b77c4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 12:50:31 +1100 Subject: [PATCH 16/38] Updates OwinStartup and split the methods into an extension methods file complete with documentation on how to implement the providers. Tested the microsoft provider. Now to clean things up: remove the 3rd party package installs to be ready for shipping, ensure that the user parts are extensible enough for people to plugin their own interfaces. --- src/Umbraco.Web.UI.Client/src/less/login.less | 82 ++++--- src/Umbraco.Web.UI.Client/src/less/panel.less | 4 +- .../src/views/common/dialogs/login.html | 50 +++-- src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 208 +++--------------- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 + src/Umbraco.Web.UI/packages.config | 1 + .../Security/Identity/AppBuilderExtensions.cs | 10 +- .../Security/Identity/NaiveSessionCache.cs | 84 ------- src/Umbraco.Web/Umbraco.Web.csproj | 7 - src/Umbraco.Web/packages.config | 1 - 10 files changed, 122 insertions(+), 329 deletions(-) delete mode 100644 src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs diff --git a/src/Umbraco.Web.UI.Client/src/less/login.less b/src/Umbraco.Web.UI.Client/src/less/login.less index d46bb41317..d01dec8733 100644 --- a/src/Umbraco.Web.UI.Client/src/less/login.less +++ b/src/Umbraco.Web.UI.Client/src/less/login.less @@ -2,46 +2,72 @@ // ------------------------- .login-overlay { - width: 100%; - height: 100%; - background: @blackLight url(../img/application/logo.png) no-repeat 25px 30px fixed !important; - background-size: 30px 30px !important; - color: @white; - position: absolute; - z-index: 2000; - top: 0px; - left: 0px; - margin: 0 !Important; - padding: 0; - border-radius: 0 + width: 100%; + height: 100%; + background: @blackLight url(../img/application/logo.png) no-repeat 25px 30px fixed !important; + background-size: 30px 30px !important; + color: @white; + position: absolute; + z-index: 2000; + top: 0px; + left: 0px; + margin: 0 !Important; + padding: 0; + border-radius: 0; } -.login-overlay .umb-modalcolumn{ - background: none; - border: none; +.login-overlay .umb-modalcolumn { + background: none; + border: none; } .login-overlay .form { - display: block; - padding-top: 100px; - padding-left: 165px; - width: 370px; - text-align: right + display: block; + padding-top: 100px; + padding-left: 165px; + width: 370px; + text-align: right; } .login-overlay h1 { - display: block; - text-align: right; - color: @white; - font-size: 18px; - font-weight: normal + display: block; + text-align: right; + color: @white; + font-size: 18px; + font-weight: normal; } -.login-overlay .alert.alert-error{ +.login-overlay .alert.alert-error { display: inline-block; - width: 270px; padding-right: 6px; padding-left: 6px; margin-top: 10px; text-align: center; -} \ No newline at end of file +} + +#hrOr { + height: 30px; + text-align: center; + position: relative; + padding-top: 20px; +} + +#hrOr hr { + margin: 0px; + border: none; + background-color: @gray; + height: 1px; +} + +#hrOr div { + background-color: black; + position: relative; + top: -16px; + border: 1px solid @gray; + padding: 4px; + border-radius: 50%; + width: 20px; + height: 20px; + margin: auto; + color: @grayLight; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 62a5d21ac7..c661fe1223 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -282,9 +282,9 @@ .umb-panel a.text-success:hover, .umb-panel a.text-success:focus { color: darken(@formSuccessText, 10%); } -.umb-user-panel .external-logins form { +.external-logins form { margin:0; } -.umb-user-panel .external-logins button { +.external-logins button { margin:5px; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html index dd7971b678..25a8b28976 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/login.html @@ -7,6 +7,33 @@ Log in below. Log in below

+ +
+ +
+ {{error}} +
+ +
+ +
+ + + +
+
+ +
+
Or
+
+ +
@@ -24,27 +51,6 @@
-
- -

External login providers

- -
- {{error}} -
- -
- -
- - - -
-
+
\ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index 930aed24b0..431cc35786 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -1,12 +1,4 @@ -using System; -using System.Globalization; -using System.Net.Http; -using System.Threading.Tasks; -using System.Web; -using Microsoft.IdentityModel.Clients.ActiveDirectory; -using Microsoft.Owin; -using Microsoft.Owin.Security.Google; -using Microsoft.Owin.Security.OpenIdConnect; +using Microsoft.Owin; using Owin; using Umbraco.Core; using Umbraco.Core.Security; @@ -19,193 +11,47 @@ namespace Umbraco.Web.UI { /// - /// Summary description for Startup + /// Default OWIN startup class /// public class OwinStartup { - public async Task DoStuff() - { - var client = new HttpClient(); - - using (var request = await client.PostAsJsonAsync("", "123")) - { - - } - } - public void Configuration(IAppBuilder app) { - - - - - - //Single method to configure the Identity user manager for use with Umbraco + //Single method to configure the Identity user manager for use with Umbraco Back office app.ConfigureUserManagerForUmbracoBackOffice( ApplicationContext.Current, Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); - //// Enable the application to use a cookie to store information for the - //// signed in user and to use a cookie to temporarily store information - //// about a user logging in with a third party login provider - //// Configure the sign in cookie - //app.UseCookieAuthentication(new CookieAuthenticationOptions - //{ - // AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, - - // Provider = new CookieAuthenticationProvider - // { - // // 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 = SecurityStampValidator - // .OnValidateIdentity, UmbracoApplicationUser, int>( - // TimeSpan.FromMinutes(30), - // (manager, user) => user.GenerateUserIdentityAsync(manager), - // identity => identity.GetUserId()) - // } - //}); - - //Ensure owin is configured for Umbraco back office authentication - this must - // be configured AFTER the standard UseCookieConfiguration above. + //Ensure owin is configured for Umbraco back office authentication. If you have any front-end OWIN + // cookie configuration, this must be declared after it. app .UseUmbracoBackOfficeCookieAuthentication() .UseUmbracoBackOfficeExternalCookieAuthentication(); - //app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie); - - //app.UseGoogleAuthentication( - // clientId: "1072120697051-07jlhgrd5hodsfe7dgqimdie8qc1omet.apps.googleusercontent.com", - // clientSecret: "Ue9swN0lEX9rwxzQz1Y_tFzg"); + /* + * Configure external logins: + * + * Depending on the authentication sources you would like to enable, you will need to install + * certain Nuget packages. + * + * For Google auth: Install-Package Microsoft.Owin.Security.Google + * For Facebook auth: Install-Package Microsoft.Owin.Security.Facebook + * For Microsoft auth: Install-Package Microsoft.Owin.Security.MicrosoftAccount + * + * There are many more providers such as Twitter, Yahoo, ActiveDirectory, etc... most information can + * be found here: http://www.asp.net/web-api/overview/security/external-authentication-services + * + * The source for these methods is located in ~/App_Code/IdentityAuthExtensions.cs, you will need to un-comment + * the methods that you would like to use. Each method contains documentation and links to + * documentation for reference. You can also tweak the code in those extension + * methods to suit your needs. + */ - var googleOptions = new GoogleOAuth2AuthenticationOptions - { - - }; - googleOptions.Description.Properties["SocialStyle"] = "btn-google-plus"; - googleOptions.Description.Properties["SocialIcon"] = "fa-google-plus"; - googleOptions.Caption = "Google"; - app.UseGoogleAuthentication(googleOptions); - - //AD docs are here: - // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet - - var authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant); - var adOptions = new OpenIdConnectAuthenticationOptions - { - //NOTE: This by default is 'OpenIdConnect' but that doesn't match what identity actually stores in the - // loginProvider field in the database which is something like: https://sts.windows.net/1234.... - // which is something based on your AD setup. This value needs to match in order for accounts to detected as linked/un-linked - // in the back office. - AuthenticationType = "https://sts.windows.net/3bb0b4c5-364f-4394-ad36-0f29f95e5ddd/", - - ClientId = clientId, - Authority = authority, - PostLogoutRedirectUri = postLoginRedirectUri, - Notifications = new OpenIdConnectAuthenticationNotifications() - { - // - // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. - // - AuthorizationCodeReceived = (context) => - { - var code = context.Code; - - var credential = new ClientCredential(clientId, appKey); - var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; - var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); - AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode( - code, - //NOTE: This URL needs to match EXACTLY the same path that is configured in the AD - // configuration. - new Uri( - HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + - HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), - credential, - graphResourceId); - - return Task.FromResult(0); - } - - } - - }; - adOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; - adOptions.Description.Properties["SocialIcon"] = "fa-windows"; - adOptions.Caption = "Active Directory"; - app.UseOpenIdConnectAuthentication(adOptions); - - } - - - } - - //NOTE: Not sure exactly what this is for but it is found in the AD source demo: - // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs - public class NaiveSessionCache : TokenCache - { - private static readonly object FileLock = new object(); - readonly string _userObjectId = string.Empty; - readonly string _cacheId = string.Empty; - public NaiveSessionCache(string userId) - { - _userObjectId = userId; - _cacheId = _userObjectId + "_TokenCache"; - - this.AfterAccess = AfterAccessNotification; - this.BeforeAccess = BeforeAccessNotification; - Load(); - } - - public void Load() - { - lock (FileLock) - { - this.Deserialize((byte[])HttpContext.Current.Session[_cacheId]); - } - } - - public void Persist() - { - lock (FileLock) - { - // reflect changes in the persistent store - HttpContext.Current.Session[_cacheId] = this.Serialize(); - // once the write operation took place, restore the HasStateChanged bit to false - this.HasStateChanged = false; - } - } - - // Empties the persistent store. - public override void Clear() - { - base.Clear(); - System.Web.HttpContext.Current.Session.Remove(_cacheId); - } - - public override void DeleteItem(TokenCacheItem item) - { - base.DeleteItem(item); - Persist(); - } - - // Triggered right before ADAL needs to access the cache. - // Reload the cache from the persistent store in case it changed since the last access. - void BeforeAccessNotification(TokenCacheNotificationArgs args) - { - Load(); - } - - // Triggered right after ADAL accessed the cache. - void AfterAccessNotification(TokenCacheNotificationArgs args) - { - // if the access operation resulted in a cache update - if (this.HasStateChanged) - { - Persist(); - } + //app.ConfigureGoogleAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); + //app.ConfigureFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); + //app.ConfigureMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); + //app.ConfigureActiveDirectory("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); } } - } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 4b8e33ca86..0c74a63e4b 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -191,6 +191,9 @@ False ..\packages\Microsoft.Owin.Security.Google.3.0.1\lib\net45\Microsoft.Owin.Security.Google.dll + + ..\packages\Microsoft.Owin.Security.MicrosoftAccount.3.0.1\lib\net45\Microsoft.Owin.Security.MicrosoftAccount.dll + False ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll @@ -380,6 +383,7 @@ Properties\SolutionInfo.cs + loadStarterKits.ascx ASPXCodeBehind diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index c0e0034b61..62c48ef30a 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -31,6 +31,7 @@ + diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 0e8712ed07..67a3625f34 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -16,6 +16,7 @@ namespace Umbraco.Web.Security.Identity { public static class AppBuilderExtensions { + #region Backoffice /// /// Configure Identity User Manager for Umbraco /// @@ -32,8 +33,8 @@ namespace Umbraco.Web.Security.Identity //Configure Umbraco user manager to be created per request app.CreatePerOwinContext( (options, owinContext) => BackOfficeUserManager.Create( - options, - owinContext, + options, + owinContext, appContext.Services.UserService, appContext.Services.ExternalLoginService, userMembershipProvider)); @@ -58,7 +59,7 @@ namespace Umbraco.Web.Security.Identity GlobalSettings.UseSSL) { Provider = new CookieAuthenticationProvider - { + { //// 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. @@ -99,7 +100,8 @@ namespace Umbraco.Web.Security.Identity }); return app; - } + } + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs b/src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs deleted file mode 100644 index f4df1fbb30..0000000000 --- a/src/Umbraco.Web/Security/Identity/NaiveSessionCache.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using System.Web; -using Microsoft.IdentityModel.Clients.ActiveDirectory; - -namespace Umbraco.Web.Security.Identity -{ - //NOTE: Not sure exactly what this is for but it is found in the AD source demo: - // https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs - // apparently it is needed for AD auth, so we'll put it here for people to use. - // It would appear that this is better for whatever reason: https://github.com/OfficeDev/O365-WebApp-SingleTenant/blob/master/O365-WebApp-SingleTenant/Models/ADALTokenCache.cs - // and please note that that link came from finding this thread: https://twitter.com/chakkaradeep/status/544962341528285184 - - /// - /// This is required to initialize the AD Identity provider on startup - /// - public class NaiveSessionCache : TokenCache - { - private static readonly object FileLock = new object(); - readonly string _userObjectId = string.Empty; - readonly string _cacheId = string.Empty; - public NaiveSessionCache(string userId) - { - _userObjectId = userId; - _cacheId = _userObjectId + "_TokenCache"; - - this.AfterAccess = AfterAccessNotification; - this.BeforeAccess = BeforeAccessNotification; - Load(); - } - - public void Load() - { - lock (FileLock) - { - this.Deserialize((byte[])HttpContext.Current.Session[_cacheId]); - } - } - - public void Persist() - { - lock (FileLock) - { - // reflect changes in the persistent store - HttpContext.Current.Session[_cacheId] = this.Serialize(); - // once the write operation took place, restore the HasStateChanged bit to false - this.HasStateChanged = false; - } - } - - // Empties the persistent store. - public override void Clear() - { - base.Clear(); - System.Web.HttpContext.Current.Session.Remove(_cacheId); - } - - public override void DeleteItem(TokenCacheItem item) - { - base.DeleteItem(item); - Persist(); - } - - // Triggered right before ADAL needs to access the cache. - // Reload the cache from the persistent store in case it changed since the last access. - void BeforeAccessNotification(TokenCacheNotificationArgs args) - { - Load(); - } - - // Triggered right after ADAL accessed the cache. - void AfterAccessNotification(TokenCacheNotificationArgs args) - { - // if the access operation resulted in a cache update - if (this.HasStateChanged) - { - Persist(); - } - } - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 79c59c85a4..0b8fea4a2f 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -138,12 +138,6 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll - - ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll - - - ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll - False ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll @@ -560,7 +554,6 @@ - diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index d1898c46fd..bd20574668 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -18,7 +18,6 @@ - From 3efd038906b3b1f63291d0fc00ab55930fd51705 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 13:13:06 +1100 Subject: [PATCH 17/38] implements IUserRoleStore for sections for users --- .../Security/BackOfficeUserManager.cs | 10 +-- .../Security/BackOfficeUserStore.cs | 76 ++++++++++++++++++- 2 files changed, 75 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index b65b9dfaa1..f84d333843 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -22,7 +22,7 @@ namespace Umbraco.Core.Security { } - #region What we support currently + #region What we support do not currently //NOTE: Not sure if we really want/need to ever support this public override bool SupportsUserClaim @@ -30,12 +30,6 @@ namespace Umbraco.Core.Security get { return false; } } - //TODO: Support this - public override bool SupportsUserRole - { - get { return false; } - } - //TODO: Support this public override bool SupportsQueryableUsers { @@ -110,7 +104,7 @@ namespace Umbraco.Core.Security //custom identity factory for creating the identity object for which we auth against in the back office manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); - //NOTE: Not implementing these currently + //NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to suport it //// Register two factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user //// You can write your own provider and plug in here. diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 6dd4aadc3e..dd2041fd51 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -15,9 +15,8 @@ namespace Umbraco.Core.Security IUserStore, IUserPasswordStore, IUserEmailStore, - IUserLoginStore - - //IUserRoleStore, + IUserLoginStore, + IUserRoleStore //TODO: This will require additional columns/tables //IUserLockoutStore @@ -442,5 +441,76 @@ namespace Umbraco.Core.Security return anythingChanged; } + /// + /// Adds a user to a role (section) + /// + /// + /// + public Task AddToRoleAsync(BackOfficeIdentityUser user, string roleName) + { + if (user.AllowedApplications.InvariantContains(roleName)) return Task.FromResult(0); + + var asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + } + + var found = _userService.GetUserById(asInt.Result); + + if (found != null) + { + found.AddAllowedSection(roleName); + _userService.Save(found); + } + + return Task.FromResult(0); + } + + /// + /// Removes the role (allowed section) for the user + /// + /// + /// + public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string roleName) + { + if (user.AllowedApplications.InvariantContains(roleName) == false) return Task.FromResult(0); + + var asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + } + + var found = _userService.GetUserById(asInt.Result); + + if (found != null) + { + found.RemoveAllowedSection(roleName); + _userService.Save(found); + } + + return Task.FromResult(0); + } + + /// + /// Returns the roles for this user + /// + /// + /// + public Task> GetRolesAsync(BackOfficeIdentityUser user) + { + return Task.FromResult((IList)user.AllowedApplications.ToList()); + } + + /// + /// Returns true if a user is in the role + /// + /// + /// + public Task IsInRoleAsync(BackOfficeIdentityUser user, string roleName) + { + return Task.FromResult(user.AllowedApplications.InvariantContains(roleName)); + } } } \ No newline at end of file From b269760b21510f0a353a8f20063ed9bed7ba6da7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 13:16:32 +1100 Subject: [PATCH 18/38] removes the BackOfficeRoleManager since we don't use roles in the back office (sections i suppose) and we can't dynamically just create them, that doesn't make sense. --- .../Models/Identity/BackOfficeIdentityRole.cs | 22 ------ .../Security/BackOfficeRoleManager.cs | 27 -------- .../Security/BackOfficeRoleStore.cs | 67 ------------------- src/Umbraco.Core/Umbraco.Core.csproj | 3 - .../Security/Identity/AppBuilderExtensions.cs | 7 +- 5 files changed, 3 insertions(+), 123 deletions(-) delete mode 100644 src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs delete mode 100644 src/Umbraco.Core/Security/BackOfficeRoleManager.cs delete mode 100644 src/Umbraco.Core/Security/BackOfficeRoleStore.cs diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs deleted file mode 100644 index 9fb4c51f52..0000000000 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityRole.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.AspNet.Identity; - -namespace Umbraco.Core.Models.Identity -{ - public class BackOfficeIdentityRole : IRole - { - public BackOfficeIdentityRole(string id) - { - Id = id; - } - - /// - /// Id of the role - /// - public string Id { get; private set; } - - /// - /// Name of the role - /// - public string Name { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeRoleManager.cs b/src/Umbraco.Core/Security/BackOfficeRoleManager.cs deleted file mode 100644 index c49a6b943b..0000000000 --- a/src/Umbraco.Core/Security/BackOfficeRoleManager.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.Owin; -using Umbraco.Core.Models.Identity; - -namespace Umbraco.Core.Security -{ - public class BackOfficeRoleManager : RoleManager - { - /// - /// Constructor - /// - /// The IRoleStore is responsible for commiting changes via the UpdateAsync/CreateAsync methods - public BackOfficeRoleManager(IRoleStore store) : base(store) - { - } - - public static BackOfficeRoleManager Create( - IdentityFactoryOptions options) - { - //TODO: Set this up! - - var manager = new BackOfficeRoleManager(new BackOfficeRoleStore()); - - return manager; - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeRoleStore.cs b/src/Umbraco.Core/Security/BackOfficeRoleStore.cs deleted file mode 100644 index 603f2e1ed2..0000000000 --- a/src/Umbraco.Core/Security/BackOfficeRoleStore.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNet.Identity; -using Umbraco.Core.Models.Identity; - -namespace Umbraco.Core.Security -{ - public class BackOfficeRoleStore : DisposableObject, IRoleStore - { - /// - /// Handles the disposal of resources. Derived from abstract class which handles common required locking logic. - /// - protected override void DisposeResources() - { - } - - /// - /// Create a new role - /// - /// - /// - public Task CreateAsync(BackOfficeIdentityRole role) - { - throw new NotImplementedException(); - } - - /// - /// Update a role - /// - /// - /// - public Task UpdateAsync(BackOfficeIdentityRole role) - { - throw new NotImplementedException(); - } - - /// - /// Delete a role - /// - /// - /// - public Task DeleteAsync(BackOfficeIdentityRole role) - { - throw new NotImplementedException(); - } - - /// - /// Find a role by id - /// - /// - /// - public Task FindByIdAsync(string roleId) - { - throw new NotImplementedException(); - } - - /// - /// Find a role by name - /// - /// - /// - public Task FindByNameAsync(string roleName) - { - throw new NotImplementedException(); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 52555fd9db..c62c869e4e 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -345,7 +345,6 @@ - @@ -420,8 +419,6 @@ - - diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 67a3625f34..63d4efd2e6 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -23,7 +23,9 @@ namespace Umbraco.Web.Security.Identity /// /// /// - public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, ApplicationContext appContext, MembershipProviderBase userMembershipProvider) + public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, + ApplicationContext appContext, + MembershipProviderBase userMembershipProvider) { //Don't proceed if the app is not ready if (appContext.IsConfigured == false @@ -38,9 +40,6 @@ namespace Umbraco.Web.Security.Identity appContext.Services.UserService, appContext.Services.ExternalLoginService, userMembershipProvider)); - - //Configure Umbraco role manager to be created per request - app.CreatePerOwinContext((options, owinContext) => BackOfficeRoleManager.Create(options)); } /// From 5a88ff774cc9178ab03047154ddc1d19da7cdb15 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 13:36:52 +1100 Subject: [PATCH 19/38] adds overload to specify custom backoffice user store for custom implementations (i.e. 2 factor auth, etc...) --- .../Security/BackOfficeUserManager.cs | 44 +++++++++++++++++-- .../src/views/common/dialogs/user.html | 2 +- .../Security/Identity/AppBuilderExtensions.cs | 29 +++++++++++- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index f84d333843..e4c58ed6f0 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -61,20 +61,58 @@ namespace Umbraco.Core.Security } #endregion + /// + /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager + /// + /// + /// + /// + /// + /// public static BackOfficeUserManager Create( IdentityFactoryOptions options, - IOwinContext context, IUserService userService, IExternalLoginService externalLoginService, MembershipProviderBase membershipProvider) { if (options == null) throw new ArgumentNullException("options"); - if (context == null) throw new ArgumentNullException("context"); if (userService == null) throw new ArgumentNullException("userService"); if (externalLoginService == null) throw new ArgumentNullException("externalLoginService"); var manager = new BackOfficeUserManager(new BackOfficeUserStore(userService, externalLoginService, membershipProvider)); + return InitUserManager(manager, membershipProvider, options); + } + + /// + /// Creates a BackOfficeUserManager instance with all default options and a custom BackOfficeUserManager instance + /// + /// + /// + /// + /// + public static BackOfficeUserManager Create( + IdentityFactoryOptions options, + BackOfficeUserStore customUserStore, + MembershipProviderBase membershipProvider) + { + if (options == null) throw new ArgumentNullException("options"); + if (customUserStore == null) throw new ArgumentNullException("customUserStore"); + + var manager = new BackOfficeUserManager(customUserStore); + + return InitUserManager(manager, membershipProvider, options); + } + + /// + /// Initializes the user manager with the correct options + /// + /// + /// + /// + /// + private static BackOfficeUserManager InitUserManager(BackOfficeUserManager manager, MembershipProviderBase membershipProvider, IdentityFactoryOptions options) + { // Configure validation logic for usernames manager.UserValidator = new UserValidator(manager) { @@ -102,7 +140,7 @@ namespace Umbraco.Core.Security } //custom identity factory for creating the identity object for which we auth against in the back office - manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); + manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); //NOTE: Not implementing these, if people need custom 2 factor auth, they'll need to implement their own UserStore to suport it diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html index 37751bfab6..8346acfabb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dialogs/user.html @@ -30,7 +30,7 @@

-
+
External login providers
diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 63d4efd2e6..912b19fd2e 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -17,8 +17,9 @@ namespace Umbraco.Web.Security.Identity public static class AppBuilderExtensions { #region Backoffice + /// - /// Configure Identity User Manager for Umbraco + /// Configure Default Identity User Manager for Umbraco /// /// /// @@ -36,12 +37,36 @@ namespace Umbraco.Web.Security.Identity app.CreatePerOwinContext( (options, owinContext) => BackOfficeUserManager.Create( options, - owinContext, appContext.Services.UserService, appContext.Services.ExternalLoginService, userMembershipProvider)); } + /// + /// Configure a custom UserStore with the Identity User Manager for Umbraco + /// + /// + /// + /// + /// + public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, + ApplicationContext appContext, + MembershipProviderBase userMembershipProvider, + BackOfficeUserStore customUserStore) + { + //Don't proceed if the app is not ready + if (appContext.IsConfigured == false + || appContext.DatabaseContext == null + || appContext.DatabaseContext.IsDatabaseConfigured == false) return; + + //Configure Umbraco user manager to be created per request + app.CreatePerOwinContext( + (options, owinContext) => BackOfficeUserManager.Create( + options, + customUserStore, + userMembershipProvider)); + } + /// /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline /// From 1f9594eef42200730fd5fca53b1aa42f7b8211a6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 13:38:05 +1100 Subject: [PATCH 20/38] updates notes in OwinStartup --- src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index 431cc35786..6d35ec5975 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -9,16 +9,15 @@ using Umbraco.Web.UI; namespace Umbraco.Web.UI { - /// /// Default OWIN startup class /// public class OwinStartup { - public void Configuration(IAppBuilder app) { - //Single method to configure the Identity user manager for use with Umbraco Back office + //Configure the Identity user manager for use with Umbraco Back office + // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) app.ConfigureUserManagerForUmbracoBackOffice( ApplicationContext.Current, Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); From 4d50dcea619fbf4f0e42734bdfe9ba22f1433a4b Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 13:46:32 +1100 Subject: [PATCH 21/38] adds the auth extensions and removes all 3rd party packages --- .../App_Code/IdentityAuthExtensions.cs | 294 ++++++++++++++++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 27 -- src/Umbraco.Web.UI/packages.config | 7 - 3 files changed, 294 insertions(+), 34 deletions(-) create mode 100644 src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs diff --git a/src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs b/src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs new file mode 100644 index 0000000000..c3aaf9345d --- /dev/null +++ b/src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using System.Web; +using Microsoft.Owin; +using Owin; +using Umbraco.Core; +//using Microsoft.Owin.Security.MicrosoftAccount; +//using Microsoft.IdentityModel.Clients.ActiveDirectory; +//using Microsoft.Owin.Security.Facebook; +//using Microsoft.Owin.Security.Google; +//using Microsoft.Owin.Security.OpenIdConnect; + +namespace Umbraco.Web.UI +{ + public static class IdentityAuthExtensions + { + + /* + + /// + /// Configure microsoft account sign-in + /// + /// + /// + /// + /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.MicrosoftAccount + /// + /// Microsoft account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#MICROSOFT + /// http://blogs.msdn.com/b/webdev/archive/2012/09/19/configuring-your-asp-net-application-for-microsoft-oauth-account.aspx + /// + /// Microsoft apps can be created here: + /// + /// http://go.microsoft.com/fwlink/?LinkID=144070 + /// + /// + public static void ConfigureMicrosoftAuth(this IAppBuilder app, string clientId, string clientSecret) + { + var msOptions = new MicrosoftAccountAuthenticationOptions + { + ClientId = clientId, + ClientSecret = clientSecret + }; + //Defines styles for buttons + msOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; + msOptions.Description.Properties["SocialIcon"] = "fa-windows"; + msOptions.Caption = "Microsoft"; + app.UseMicrosoftAccountAuthentication(msOptions); + } + + */ + + /* + + /// + /// Configure google sign-in + /// + /// + /// + /// + /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.Google + /// + /// Google account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE + /// + /// Google apps can be created here: + /// + /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials + /// + /// + public static void ConfigureGoogleAuth(this IAppBuilder app, string clientId, string clientSecret) + { + var googleOptions = new GoogleOAuth2AuthenticationOptions + { + ClientId = clientId, + ClientSecret = clientSecret + }; + //Defines styles for buttons + googleOptions.Description.Properties["SocialStyle"] = "btn-google-plus"; + googleOptions.Description.Properties["SocialIcon"] = "fa-google-plus"; + googleOptions.Caption = "Google"; + app.UseGoogleAuthentication(googleOptions); + } + + */ + + /* + + /// + /// Configure facebook sign-in + /// + /// + /// + /// + /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.Facebook + /// + /// Facebook account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#FACEBOOK + /// + /// Facebook apps can be created here: + /// + /// https://developers.facebook.com/ + /// + /// + public static void ConfigureFacebookAuth(this IAppBuilder app, string appId, string appSecret) + { + var fbOptions = new FacebookAuthenticationOptions + { + AppId = appId, + AppSecret = appSecret, + }; + //Defines styles for buttons + fbOptions.Description.Properties["SocialStyle"] = "btn-facebook"; + fbOptions.Description.Properties["SocialIcon"] = "fa-facebook"; + fbOptions.Caption = "Facebook"; + app.UseFacebookAuthentication(fbOptions); + } + + */ + + + /* + + /// + /// Configure ActiveDirectory sign-in + /// + /// + /// + /// + /// + /// The URL that will be redirected to after login is successful, example: http://mydomain.com/umbraco/; + /// + /// + /// + /// This by default is 'OpenIdConnect' but that doesn't match what ASP.Net Identity actually stores in the + /// loginProvider field in the database which looks something like this (for example): + /// https://sts.windows.net/3bb0b4c5-364f-4394-ad36-0f29f95e5ggg/ + /// and is based on your AD setup. This value needs to match in order for accounts to + /// detected as linked/un-linked in the back office. + /// + /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.OpenIdConnect + /// Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory + /// + /// ActiveDirectory account documentation for ASP.Net Identity can be found: + /// + /// https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet + /// + /// This configuration requires the NaiveSessionCache class below which will need to be un-commented + /// + /// + public static void ConfigureActiveDirectory(this IAppBuilder app, + string tenant, string clientId, string postLoginRedirectUri, string appKey, + string authType) + { + const string aadInstance = "https://login.windows.net/{0}"; + const string graphResourceId = "https://graph.windows.net"; + + var authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant); + var adOptions = new OpenIdConnectAuthenticationOptions + { + AuthenticationType = authType, + ClientId = clientId, + Authority = authority, + PostLogoutRedirectUri = postLoginRedirectUri, + Notifications = new OpenIdConnectAuthenticationNotifications() + { + // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. + AuthorizationCodeReceived = (context) => + { + var credential = new ClientCredential(clientId, appKey); + var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; + var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); + var result = authContext.AcquireTokenByAuthorizationCode( + context.Code, + //NOTE: This URL needs to match EXACTLY the same path that is configured in the AD configuration. + new Uri( + HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + + HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), + credential, + graphResourceId); + + return Task.FromResult(0); + } + + } + + }; + adOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; + adOptions.Description.Properties["SocialIcon"] = "fa-windows"; + adOptions.Caption = "Active Directory"; + app.UseOpenIdConnectAuthentication(adOptions); + } + + */ + } + + /* + + /// + /// A Session cache token storage which is required to initialize the AD Identity provider on startup + /// + /// + /// Based on the examples from the AD samples: + /// https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs + /// + /// There are some newer examples of different token storage including persistent storage here: + /// It would appear that this is better for whatever reason: https://github.com/OfficeDev/O365-WebApp-SingleTenant/blob/master/O365-WebApp-SingleTenant/Models/ADALTokenCache.cs + /// + /// The type of token storage will be dependent on your requirements but this should be fine for standard installations + /// + public class NaiveSessionCache : TokenCache + { + private static readonly object FileLock = new object(); + readonly string _cacheId; + public NaiveSessionCache(string userId) + { + _cacheId = userId + "_TokenCache"; + + AfterAccess = AfterAccessNotification; + BeforeAccess = BeforeAccessNotification; + Load(); + } + + public void Load() + { + lock (FileLock) + { + Deserialize((byte[])HttpContext.Current.Session[_cacheId]); + } + } + + public void Persist() + { + lock (FileLock) + { + // reflect changes in the persistent store + HttpContext.Current.Session[_cacheId] = Serialize(); + // once the write operation took place, restore the HasStateChanged bit to false + HasStateChanged = false; + } + } + + // Empties the persistent store. + public override void Clear() + { + base.Clear(); + HttpContext.Current.Session.Remove(_cacheId); + } + + public override void DeleteItem(TokenCacheItem item) + { + base.DeleteItem(item); + Persist(); + } + + // Triggered right before ADAL needs to access the cache. + // Reload the cache from the persistent store in case it changed since the last access. + void BeforeAccessNotification(TokenCacheNotificationArgs args) + { + Load(); + } + + // Triggered right after ADAL accessed the cache. + void AfterAccessNotification(TokenCacheNotificationArgs args) + { + // if the access operation resulted in a cache update + if (HasStateChanged) + { + Persist(); + } + } + } + + */ +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 0c74a63e4b..3d58ed19bd 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -158,16 +158,6 @@ ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll - - ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll - - - ..\packages\Microsoft.IdentityModel.Clients.ActiveDirectory.2.14.201151115\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll - - - False - ..\packages\Microsoft.IdentityModel.Protocol.Extensions.1.0.1\lib\net45\Microsoft.IdentityModel.Protocol.Extensions.dll - ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll True @@ -184,23 +174,10 @@ False ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll - - ..\packages\Microsoft.Owin.Security.Facebook.3.0.1\lib\net45\Microsoft.Owin.Security.Facebook.dll - - - False - ..\packages\Microsoft.Owin.Security.Google.3.0.1\lib\net45\Microsoft.Owin.Security.Google.dll - - - ..\packages\Microsoft.Owin.Security.MicrosoftAccount.3.0.1\lib\net45\Microsoft.Owin.Security.MicrosoftAccount.dll - False ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll - - ..\packages\Microsoft.Owin.Security.OpenIdConnect.3.0.1\lib\net45\Microsoft.Owin.Security.OpenIdConnect.dll - ..\packages\Microsoft.Bcl.Async.1.0.165\lib\net45\Microsoft.Threading.Tasks.dll @@ -253,10 +230,6 @@ - - False - ..\packages\System.IdentityModel.Tokens.Jwt.4.0.1\lib\net45\System.IdentityModel.Tokens.Jwt.dll - False diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 62c48ef30a..e5d15942f1 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -22,18 +22,12 @@ - - - - - - @@ -41,6 +35,5 @@ - \ No newline at end of file From fc2b3d7fc7f659077504510e30dd0cdd7d67fa6d Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 14:23:55 +1100 Subject: [PATCH 22/38] fixes merge issues --- src/Umbraco.Web.UI.Client/bower.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/bower.json b/src/Umbraco.Web.UI.Client/bower.json index ce312d669a..ed2b0d7332 100644 --- a/src/Umbraco.Web.UI.Client/bower.json +++ b/src/Umbraco.Web.UI.Client/bower.json @@ -19,11 +19,11 @@ "typeahead.js": "~0.10.5", "underscore": "~1.7.0", "rgrove-lazyload": "*", - "bootstrap-social": "~4.8.0" + "bootstrap-social": "~4.8.0", "jquery": "2.0.3", "jquery-file-upload": "~9.4.0", "jquery-ui": "1.10.3", - "angular-dynamic-locale": "~0.1.27" + "angular-dynamic-locale": "~0.1.27" }, "exportsOverride": { "rgrove-lazyload": { @@ -35,18 +35,21 @@ "underscore": { "": "underscore-min.{js,map}" }, + "angular-dynamic-locale": { + "": "tmhDynamicLocale.min.{js,js.map}" + }, "bootstrap-social": { "": "bootstrap-social.css" }, "font-awesome": { "css": "css/font-awesome.min.css", - "fonts" : "fonts/*" + "fonts": "fonts/*" }, "bootstrap": { "ignore": "*.ignore" }, "jquery": { - "": "jquery.min.{js,map}" + "": "jquery.min.{js,map}" }, "jquery-file-upload": { "": "**/jquery.{fileupload,fileupload-process,fileupload-angular,fileupload-image}.js" @@ -61,7 +64,7 @@ "blueimp-tmpl": { "ignore": "*.ignore" }, - + "blueimp-canvas-to-blob": { "ignore": "*.ignore" } From 90b562a0a1d28e98164f7da21af2c58e46120dd0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 24 Mar 2015 20:17:37 +1100 Subject: [PATCH 23/38] Update the PostLogin method to write the auth ticket the way that webapi is supposed to, not sure how this was actually working before because writing cookies directly with HttpContext and then also using WebApi normally doesn't work (maybe in very specific circumstances), so now the cookie writing is done consistently and it is working, prior to this i was getting lots of issues with the xsrf tokens. Updated some user model mappings for convenience and update naming conventions for some properties of the BackOfficeIdentityUser for consistency. --- .../Models/Identity/BackOfficeIdentityUser.cs | 6 +- .../Models/Identity/IdentityModelMappings.cs | 8 +- .../Security/AuthenticationExtensions.cs | 61 +++++++++++- .../BackOfficeClaimsIdentityFactory.cs | 6 +- .../Security/BackOfficeUserStore.cs | 22 ++--- .../Editors/AuthenticationController.cs | 99 +++++++++---------- .../Models/Mapping/UserModelMapper.cs | 29 +++++- .../Security/Identity/AppBuilderExtensions.cs | 2 + src/Umbraco.Web/Security/WebSecurity.cs | 23 ++--- .../Filters/AngularAntiForgeryHelper.cs | 7 +- .../UmbracoBackOfficeLogoutAttribute.cs | 2 +- 11 files changed, 175 insertions(+), 90 deletions(-) diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 3ba5b4259a..5060cb5912 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -11,9 +11,9 @@ namespace Umbraco.Core.Models.Identity /// Gets/sets the user's real name ///
public string Name { get; set; } - public int StartContentNode { get; set; } - public int StartMediaNode { get; set; } - public string[] AllowedApplications { get; set; } + public int StartContentId { get; set; } + public int StartMediaId { get; set; } + public string[] AllowedSections { get; set; } public string Culture { get; set; } public string UserTypeAlias { get; set; } diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index 8fa2703f39..def71a8982 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -18,12 +18,12 @@ namespace Umbraco.Core.Models.Identity .ForMember(user => user.LockoutEndDateUtc, expression => expression.UseValue(DateTime.MaxValue.ToUniversalTime())) .ForMember(user => user.UserName, expression => expression.MapFrom(user => user.Username)) .ForMember(user => user.PasswordHash, expression => expression.MapFrom(user => GetPasswordHash(user.RawPasswordValue))) - .ForMember(user => user.Culture, expression => expression.MapFrom(user => user.Language)) + .ForMember(user => user.Culture, expression => expression.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) .ForMember(user => user.Name, expression => expression.MapFrom(user => user.Name)) - .ForMember(user => user.StartMediaNode, expression => expression.MapFrom(user => user.StartMediaId)) - .ForMember(user => user.StartContentNode, expression => expression.MapFrom(user => user.StartContentId)) + .ForMember(user => user.StartMediaId, expression => expression.MapFrom(user => user.StartMediaId)) + .ForMember(user => user.StartContentId, expression => expression.MapFrom(user => user.StartContentId)) .ForMember(user => user.UserTypeAlias, expression => expression.MapFrom(user => user.UserType.Alias)) - .ForMember(user => user.AllowedApplications, expression => expression.MapFrom(user => user.AllowedSections.ToArray())); + .ForMember(user => user.AllowedSections, expression => expression.MapFrom(user => user.AllowedSections.ToArray())); } private string GetPasswordHash(string storedPass) diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index e71dd0e00c..ca597a8fef 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -9,9 +9,11 @@ using System.Security.Principal; using System.Threading; using System.Web; using System.Web.Security; +using AutoMapper; using Microsoft.Owin; using Newtonsoft.Json; using Umbraco.Core.Configuration; +using Umbraco.Core.Models.Membership; namespace Umbraco.Core.Security { @@ -163,8 +165,60 @@ namespace Umbraco.Core.Security Expires = DateTime.Now.AddYears(-1), Path = "/" }; + //remove the external login cookie too + var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalAuthenticationType, "") + { + Expires = DateTime.Now.AddYears(-1), + Path = "/" + }; - response.Headers.AddCookies(new[] { authCookie, prevCookie }); + response.Headers.AddCookies(new[] { authCookie, prevCookie, extLoginCookie }); + } + + /// + /// This adds the forms authentication cookie for webapi since cookies are handled differently + /// + /// + /// + public static FormsAuthenticationTicket UmbracoLoginWebApi(this HttpResponseMessage response, IUser user) + { + if (response == null) throw new ArgumentNullException("response"); + + //remove the external login cookie + var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalAuthenticationType, "") + { + Expires = DateTime.Now.AddYears(-1), + Path = "/" + }; + + var userDataString = JsonConvert.SerializeObject(Mapper.Map(user)); + + var ticket = new FormsAuthenticationTicket( + 4, + user.Username, + DateTime.Now, + DateTime.Now.AddMinutes(GlobalSettings.TimeOutInMinutes), + true, + userDataString, + "/" + ); + + // Encrypt the cookie using the machine key for secure transport + var encrypted = FormsAuthentication.Encrypt(ticket); + + //add the cookie + var authCookie = new CookieHeaderValue(UmbracoConfig.For.UmbracoSettings().Security.AuthCookieName, encrypted) + { + //Umbraco has always persisted it's original cookie for 1 day so we'll keep it that way + Expires = DateTime.Now.AddMinutes(1440), + Path = "/", + Secure = GlobalSettings.UseSSL, + HttpOnly = true + }; + + response.Headers.AddCookies(new[] { authCookie, extLoginCookie }); + + return ticket; } /// @@ -297,8 +351,8 @@ namespace Umbraco.Core.Security private static void Logout(this HttpContextBase http, string cookieName) { if (http == null) throw new ArgumentNullException("http"); - //clear the preview cookie too - var cookies = new[] { cookieName, Constants.Web.PreviewCookieName }; + //clear the preview cookie and external login + var cookies = new[] { cookieName, Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalAuthenticationType }; foreach (var c in cookies) { //remove from the request @@ -411,7 +465,6 @@ namespace Umbraco.Core.Security /// The user data. /// The login timeout mins. /// The minutes persisted. - /// The cookie path. /// Name of the cookie. /// The cookie domain. private static FormsAuthenticationTicket CreateAuthTicketAndCookie(this HttpContextBase http, diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs index 54b537faab..b6d19b78eb 100644 --- a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -22,11 +22,11 @@ namespace Umbraco.Core.Security Id = user.Id, Username = user.UserName, RealName = user.Name, - AllowedApplications = user.AllowedApplications, + AllowedApplications = user.AllowedSections, Culture = user.Culture, Roles = user.Roles.Select(x => x.RoleId).ToArray(), - StartContentNode = user.StartContentNode, - StartMediaNode = user.StartMediaNode + StartContentNode = user.StartContentId, + StartMediaNode = user.StartMediaId }); return umbracoIdentity; diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index dd2041fd51..0b1c95deb9 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -414,25 +414,25 @@ namespace Umbraco.Core.Security anythingChanged = true; user.Language = identityUser.Culture; } - if (user.StartMediaId != identityUser.StartMediaNode) + if (user.StartMediaId != identityUser.StartMediaId) { anythingChanged = true; - user.StartMediaId = identityUser.StartMediaNode; + user.StartMediaId = identityUser.StartMediaId; } - if (user.StartContentId != identityUser.StartContentNode) + if (user.StartContentId != identityUser.StartContentId) { anythingChanged = true; - user.StartContentId = identityUser.StartContentNode; + user.StartContentId = identityUser.StartContentId; } - if (user.AllowedSections.ContainsAll(identityUser.AllowedApplications) == false - || identityUser.AllowedApplications.ContainsAll(user.AllowedSections) == false) + if (user.AllowedSections.ContainsAll(identityUser.AllowedSections) == false + || identityUser.AllowedSections.ContainsAll(user.AllowedSections) == false) { anythingChanged = true; foreach (var allowedSection in user.AllowedSections) { user.RemoveAllowedSection(allowedSection); } - foreach (var allowedApplication in identityUser.AllowedApplications) + foreach (var allowedApplication in identityUser.AllowedSections) { user.AddAllowedSection(allowedApplication); } @@ -448,7 +448,7 @@ namespace Umbraco.Core.Security /// public Task AddToRoleAsync(BackOfficeIdentityUser user, string roleName) { - if (user.AllowedApplications.InvariantContains(roleName)) return Task.FromResult(0); + if (user.AllowedSections.InvariantContains(roleName)) return Task.FromResult(0); var asInt = user.Id.TryConvertTo(); if (asInt == false) @@ -474,7 +474,7 @@ namespace Umbraco.Core.Security /// public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string roleName) { - if (user.AllowedApplications.InvariantContains(roleName) == false) return Task.FromResult(0); + if (user.AllowedSections.InvariantContains(roleName) == false) return Task.FromResult(0); var asInt = user.Id.TryConvertTo(); if (asInt == false) @@ -500,7 +500,7 @@ namespace Umbraco.Core.Security /// public Task> GetRolesAsync(BackOfficeIdentityUser user) { - return Task.FromResult((IList)user.AllowedApplications.ToList()); + return Task.FromResult((IList)user.AllowedSections.ToList()); } /// @@ -510,7 +510,7 @@ namespace Umbraco.Core.Security /// public Task IsInRoleAsync(BackOfficeIdentityUser user, string roleName) { - return Task.FromResult(user.AllowedApplications.InvariantContains(roleName)); + return Task.FromResult(user.AllowedSections.InvariantContains(roleName)); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index a1e12401b1..0606cec1dd 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -43,7 +43,14 @@ namespace Umbraco.Web.Editors [IsBackOffice] public class AuthenticationController : UmbracoApiController { - + + private BackOfficeUserManager _userManager; + + protected BackOfficeUserManager UserManager + { + get { return _userManager ?? (_userManager = TryGetOwinContext().Result.GetUserManager()); } + } + /// /// This is a special method that will return the current users' remaining session seconds, the reason /// it is special is because this route is ignored in the UmbracoModule so that the auth ticket doesn't get @@ -85,33 +92,6 @@ namespace Umbraco.Web.Editors } } - private void AddModelErrors(IdentityResult result, string prefix = "") - { - foreach (var error in result.Errors) - { - ModelState.AddModelError(prefix, error); - } - } - - private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) - { - var owinContext = TryGetOwinContext().Result; - - owinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); - - owinContext.Authentication.SignIn( - new AuthenticationProperties() { IsPersistent = isPersistent }, - await GenerateUserIdentityAsync(user)); - } - - private async Task GenerateUserIdentityAsync(BackOfficeIdentityUser user) - { - // NOTE the authenticationType must match the umbraco one - // defined in CookieAuthenticationOptions.AuthenticationType - var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); - return userIdentity; - } - /// /// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest) /// @@ -154,53 +134,45 @@ namespace Umbraco.Web.Editors } [WebApi.UmbracoAuthorize] - [SetAngularAntiForgeryTokens] + [ValidateAngularAntiForgeryToken] public async Task> GetCurrentUserLinkedLogins() { var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId()); return identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey); } - private BackOfficeUserManager _userManager; - - protected BackOfficeUserManager UserManager - { - get { return _userManager ?? (_userManager = TryGetOwinContext().Result.GetUserManager()); } - } - /// /// Logs a user in /// /// [SetAngularAntiForgeryTokens] - public UserDetail PostLogin(LoginModel loginModel) + public HttpResponseMessage PostLogin(LoginModel loginModel) { if (UmbracoContext.Security.ValidateBackOfficeCredentials(loginModel.Username, loginModel.Password)) { + //get the user var user = Security.GetBackOfficeUser(loginModel.Username); + var userDetail = Mapper.Map(user); - //TODO: Clean up the int cast! - var ticket = UmbracoContext.Security.PerformLogin(user); + //create a response with the userDetail object + var response = Request.CreateResponse(HttpStatusCode.OK, userDetail); - //TODO: Normally we'd do something like this for identity, but we're mixing and matching legacy and new here - // so we'll keep the legacy way and move forward with this in our custom handler for now, eventually replacing - // the above legacy logic with the new stuff. - - //OwinContext.Authentication.SignOut(DefaultAuthenticationTypes.ExternalCookie); - //OwinContext.Authentication.SignIn(new AuthenticationProperties() { IsPersistent = isPersistent }, - // await user.GenerateUserIdentityAsync(UserManager)); + //set the response cookies with the ticket (NOTE: This needs to be done with the custom webapi extension because + // we cannot mix HttpContext.Response.Cookies and the way WebApi/Owin work) + var ticket = response.UmbracoLoginWebApi(user); var http = this.TryGetHttpContext(); if (http.Success == false) { throw new InvalidOperationException("This method requires that an HttpContext be active"); } + //This ensure the current principal is set, otherwise any logic executing after this wouldn't actually be authenticated http.Result.AuthenticateCurrentRequest(ticket, false); + + //update the userDetail and set their remaining seconds + userDetail.SecondsUntilTimeout = ticket.GetRemainingAuthSeconds(); - var result = Mapper.Map(user); - //set their remaining seconds - result.SecondsUntilTimeout = ticket.GetRemainingAuthSeconds(); - return result; + return response; } //return BadRequest (400), we don't want to return a 401 because that get's intercepted @@ -222,5 +194,32 @@ namespace Umbraco.Web.Editors { return Request.CreateResponse(HttpStatusCode.OK); } + + private void AddModelErrors(IdentityResult result, string prefix = "") + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(prefix, error); + } + } + + private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) + { + var owinContext = TryGetOwinContext().Result; + + owinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); + + owinContext.Authentication.SignIn( + new AuthenticationProperties() { IsPersistent = isPersistent }, + await GenerateUserIdentityAsync(user)); + } + + private async Task GenerateUserIdentityAsync(BackOfficeIdentityUser user) + { + // NOTE the authenticationType must match the umbraco one + // defined in CookieAuthenticationOptions.AuthenticationType + var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); + return userIdentity; + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 0bda50e1fc..7200f2a7c2 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -5,6 +5,9 @@ using Umbraco.Core.Models.Mapping; using Umbraco.Core.Models.Membership; using Umbraco.Web.Models.ContentEditing; using umbraco; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Identity; +using Umbraco.Core.Security; namespace Umbraco.Web.Models.Mapping { @@ -17,7 +20,19 @@ namespace Umbraco.Web.Models.Mapping .ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserType.Alias)) .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) - .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => ui.Culture(user))) + .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) + .ForMember( + detail => detail.EmailHash, + opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().ToMd5())) + .ForMember(detail => detail.SecondsUntilTimeout, opt => opt.Ignore()); + + config.CreateMap() + .ForMember(detail => detail.UserId, opt => opt.MapFrom(user => user.Id)) + .ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserTypeAlias)) + .ForMember(detail => detail.StartContentId, opt => opt.MapFrom(user => user.StartContentId)) + .ForMember(detail => detail.StartMediaId, opt => opt.MapFrom(user => user.StartMediaId)) + .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.Culture)) + .ForMember(detail => detail.AllowedSections, opt => opt.MapFrom(user => user.AllowedSections)) .ForMember( detail => detail.EmailHash, opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().ToMd5())) @@ -25,6 +40,18 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(detail => detail.UserId, opt => opt.MapFrom(profile => GetIntId(profile.Id))); + + config.CreateMap() + .ConstructUsing((IUser user) => new UserData(Guid.NewGuid().ToString("N"))) //this is the 'session id' + .ForMember(detail => detail.Id, opt => opt.MapFrom(user => user.Id)) + .ForMember(detail => detail.AllowedApplications, opt => opt.MapFrom(user => user.AllowedSections)) + .ForMember(detail => detail.RealName, opt => opt.MapFrom(user => user.Name)) + .ForMember(detail => detail.Roles, opt => opt.MapFrom(user => new[] {user.UserType.Alias})) + .ForMember(detail => detail.StartContentNode, opt => opt.MapFrom(user => user.StartContentId)) + .ForMember(detail => detail.StartMediaNode, opt => opt.MapFrom(user => user.StartMediaId)) + .ForMember(detail => detail.Username, opt => opt.MapFrom(user => user.Username)) + .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))); + } private static int GetIntId(object id) diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 912b19fd2e..7e70ba2958 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -84,6 +84,8 @@ namespace Umbraco.Web.Security.Identity { Provider = new CookieAuthenticationProvider { + //TODO: Need to implement IUserSecurityStampStore on BackOfficeUserStore! + //// 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. diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index b6ec3680d8..19857ddbcf 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Security; +using AutoMapper; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; @@ -94,18 +95,18 @@ namespace Umbraco.Web.Security /// returns the Forms Auth ticket created which is used to log them in public virtual FormsAuthenticationTicket PerformLogin(IUser user) { - var ticket = _httpContext.CreateUmbracoAuthTicket(new UserData(Guid.NewGuid().ToString("N")) + //clear the external cookie - we do this without owin context because we're writing cookies directly to httpcontext + // and cookie handling is different with httpcontext vs webapi and owin, normally we'd do: + //_httpContext.GetOwinContext().Authentication.SignOut(Constants.Security.BackOfficeExternalAuthenticationType); + + var externalLoginCookie = _httpContext.Request.Cookies.Get(Constants.Security.BackOfficeExternalAuthenticationType); + if (externalLoginCookie != null) { - Id = user.Id, - AllowedApplications = user.AllowedSections.ToArray(), - RealName = user.Name, - //currently we only have one user type! - Roles = new[] { user.UserType.Alias }, - StartContentNode = user.StartContentId, - StartMediaNode = user.StartMediaId, - Username = user.Username, - Culture = ui.Culture(user) - }); + externalLoginCookie.Expires = DateTime.Now.AddYears(-1); + _httpContext.Response.Cookies.Set(externalLoginCookie); + } + + var ticket = _httpContext.CreateUmbracoAuthTicket(Mapper.Map(user)); LogHelper.Info("User Id: {0} logged in", () => user.Id); diff --git a/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs index 2e4b4176bb..d48cd66077 100644 --- a/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs +++ b/src/Umbraco.Web/WebApi/Filters/AngularAntiForgeryHelper.cs @@ -1,8 +1,10 @@ -using System.Linq; +using System; +using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Web.Helpers; using Umbraco.Core; +using Umbraco.Core.Logging; namespace Umbraco.Web.WebApi.Filters { @@ -54,8 +56,9 @@ namespace Umbraco.Web.WebApi.Filters { AntiForgery.Validate(cookieToken, headerToken); } - catch + catch (Exception ex) { + LogHelper.Error(typeof(AngularAntiForgeryHelper), "Could not validate XSRF token", ex); return false; } return true; diff --git a/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs index ecde11023b..693d45c792 100644 --- a/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/UmbracoBackOfficeLogoutAttribute.cs @@ -7,7 +7,7 @@ namespace Umbraco.Web.WebApi.Filters /// A filter that is used to remove the authorization cookie for the current user when the request is successful /// /// - /// This is used so that we can log a user out in conjunction with using other filters that modify the cookies collection. + /// This is used so that we can log a user OUT in conjunction with using other filters that modify the cookies collection. /// SD: I beleive this is a bug with web api since if you modify the cookies collection on the HttpContext.Current and then /// use a filter to write the cookie headers, the filter seems to have no affect at all. /// From 4dcc4807ed69b72035e4f40a9896644bfa73d6e7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 10:57:10 +1100 Subject: [PATCH 24/38] Implements IUserSecurityStore and ensures there is a security stamp token in place, have updated the repository layer to manual update this if ASPNet Identity APIs are not used to update users. --- .../Models/Identity/BackOfficeIdentityUser.cs | 13 + src/Umbraco.Core/Models/Membership/IUser.cs | 5 + src/Umbraco.Core/Models/Membership/User.cs | 18 ++ src/Umbraco.Core/Models/Rdbms/UserDto.cs | 5 + .../Persistence/Factories/UserFactory.cs | 10 +- .../Initial/DatabaseSchemaResult.cs | 6 + .../AddUserSecurityStampColumn.cs | 22 ++ .../Repositories/UserRepository.cs | 12 +- .../Security/BackOfficeUserManager.cs | 8 +- .../Security/BackOfficeUserStore.cs | 224 ++++++++++++------ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Editors/AuthenticationController.cs | 16 +- .../Editors/BackOfficeController.cs | 15 +- .../Security/Identity/AppBuilderExtensions.cs | 21 +- 14 files changed, 258 insertions(+), 118 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserSecurityStampColumn.cs diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 5060cb5912..31c56ba013 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -2,11 +2,24 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Umbraco.Core.Security; namespace Umbraco.Core.Models.Identity { public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim> { + + public async Task GenerateUserIdentityAsync(BackOfficeUserManager manager) + { + // NOTE the authenticationType must match the umbraco one + // defined in CookieAuthenticationOptions.AuthenticationType + var userIdentity = await manager.CreateIdentityAsync(this, Constants.Security.BackOfficeAuthenticationType); + return userIdentity; + } + /// /// Gets/sets the user's real name /// diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs index 47eb074553..f1f9c23971 100644 --- a/src/Umbraco.Core/Models/Membership/IUser.cs +++ b/src/Umbraco.Core/Models/Membership/IUser.cs @@ -38,5 +38,10 @@ namespace Umbraco.Core.Models.Membership /// Exposes the basic profile data /// IProfile ProfileData { get; } + + /// + /// The security stamp used by ASP.Net identity + /// + string SecurityStamp { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs index 3e95a94d3a..7053eaf339 100644 --- a/src/Umbraco.Core/Models/Membership/User.cs +++ b/src/Umbraco.Core/Models/Membership/User.cs @@ -58,6 +58,7 @@ namespace Umbraco.Core.Models.Membership private IUserType _userType; private string _name; + private string _securityStamp; private List _addedSections; private List _removedSections; private ObservableCollection _sectionCollection; @@ -76,6 +77,7 @@ namespace Umbraco.Core.Models.Membership private bool _defaultToLiveEditing; + private static readonly PropertyInfo SecurityStampSelector = ExpressionHelper.GetPropertyInfo(x => x.SecurityStamp); private static readonly PropertyInfo SessionTimeoutSelector = ExpressionHelper.GetPropertyInfo(x => x.SessionTimeout); private static readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentId); private static readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaId); @@ -232,6 +234,22 @@ namespace Umbraco.Core.Models.Membership get { return new UserProfile(this); } } + /// + /// The security stamp used by ASP.Net identity + /// + public string SecurityStamp + { + get { return _securityStamp; } + set + { + SetPropertyValueAndDetectChanges(o => + { + _securityStamp = value; + return _securityStamp; + }, _securityStamp, SecurityStampSelector); + } + } + /// /// Used internally to check if we need to add a section in the repository to the db /// diff --git a/src/Umbraco.Core/Models/Rdbms/UserDto.cs b/src/Umbraco.Core/Models/Rdbms/UserDto.cs index 392010e56d..316a487331 100644 --- a/src/Umbraco.Core/Models/Rdbms/UserDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/UserDto.cs @@ -51,6 +51,11 @@ namespace Umbraco.Core.Models.Rdbms [NullSetting(NullSetting = NullSettings.Null)] [Length(10)] public string UserLanguage { get; set; } + + [Column("securityStampToken")] + [NullSetting(NullSetting = NullSettings.Null)] + [Length(255)] + public string SecurityStampToken { get; set; } [ResultColumn] public List User2AppDtos { get; set; } diff --git a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs index 1f71665f50..1db30302dd 100644 --- a/src/Umbraco.Core/Persistence/Factories/UserFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UserFactory.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using Umbraco.Core.Models.Membership; @@ -32,7 +33,9 @@ namespace Umbraco.Core.Persistence.Factories IsLockedOut = dto.NoConsole, IsApproved = dto.Disabled == false, Email = dto.Email, - Language = dto.UserLanguage + Language = dto.UserLanguage, + //make it a GUID if it's empty + SecurityStamp = dto.SecurityStampToken.IsNullOrWhiteSpace() ? Guid.NewGuid().ToString() : dto.SecurityStampToken }; foreach (var app in dto.User2AppDtos) @@ -61,7 +64,8 @@ namespace Umbraco.Core.Persistence.Factories UserLanguage = entity.Language, UserName = entity.Name, Type = short.Parse(entity.UserType.Id.ToString(CultureInfo.InvariantCulture)), - User2AppDtos = new List() + User2AppDtos = new List(), + SecurityStampToken = entity.SecurityStamp }; foreach (var app in entity.AllowedSections) diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs index ca0cf7ebd2..c2acbd3c97 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaResult.cs @@ -98,6 +98,12 @@ namespace Umbraco.Core.Persistence.Migrations.Initial return new Version(7, 0, 0); } + //if the error is for umbracoAccess it must be the previous version to 7.3 since that is when it is added + if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoAccess")))) + { + return new Version(7, 2, 5); + } + return UmbracoVersion.Current; } diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserSecurityStampColumn.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserSecurityStampColumn.cs new file mode 100644 index 0000000000..21ead0996b --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddUserSecurityStampColumn.cs @@ -0,0 +1,22 @@ +using System.Linq; +using Umbraco.Core.Configuration; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero +{ + [Migration("7.3.0", 10, GlobalSettings.UmbracoMigrationName)] + public class AddUserSecurityStampColumn : MigrationBase + { + public override void Up() + { + //Don't exeucte if the column is already there + var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray(); + if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("securityStampToken"))) return; + + Create.Column("securityStampToken").OnTable("umbracoUser").AsString(255).Nullable(); + } + + public override void Down() + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index d13df47b71..b26daaee7f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -170,7 +170,8 @@ namespace Umbraco.Core.Persistence.Repositories {"userName", "Name"}, {"userLogin", "Username"}, {"userEmail", "Email"}, - {"userLanguage", "Language"} + {"userLanguage", "Language"}, + {"securityStampToken", "SecurityStamp"} }; //create list of properties that have changed @@ -183,6 +184,15 @@ namespace Umbraco.Core.Persistence.Repositories if (dirtyEntity.IsPropertyDirty("RawPasswordValue") && entity.RawPasswordValue.IsNullOrWhiteSpace() == false) { changedCols.Add("userPassword"); + + //special case - when using ASP.Net identity the user manager will take care of updating the security stamp, however + // when not using ASP.Net identity (i.e. old membership providers), we'll need to take care of updating this manually + // so we can just detect if that property is dirty, if it's not we'll set it manually + if (dirtyEntity.IsPropertyDirty("SecurityStamp") == false) + { + userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString(); + changedCols.Add("securityStampToken"); + } } //only update the changed cols diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index e4c58ed6f0..b410f34107 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -21,7 +21,7 @@ namespace Umbraco.Core.Security : base(store) { } - + #region What we support do not currently //NOTE: Not sure if we really want/need to ever support this @@ -42,12 +42,6 @@ namespace Umbraco.Core.Security get { return false; } } - //TODO: Support this - public override bool SupportsUserSecurityStamp - { - get { return false; } - } - //TODO: Support this public override bool SupportsUserTwoFactor { diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 0b1c95deb9..f6d8222c44 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -16,20 +16,22 @@ namespace Umbraco.Core.Security IUserPasswordStore, IUserEmailStore, IUserLoginStore, - IUserRoleStore + IUserRoleStore, + IUserSecurityStampStore + + //TODO: This would require additional columns/tables for now people will need to implement this on their own + //IUserPhoneNumberStore, + //IUserTwoFactorStore, //TODO: This will require additional columns/tables //IUserLockoutStore - //TODO: Implement this - might need to add a new column for this - // http://stackoverflow.com/questions/19487322/what-is-asp-net-identitys-iusersecuritystampstoretuser-interface - //IUserSecurityStampStore - - //TODO: To do this we need to implement IQueryable - seems pretty overkill? + //TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation //IQueryableUserStore { private readonly IUserService _userService; private readonly IExternalLoginService _externalLoginService; + private bool _disposed = false; public BackOfficeUserStore(IUserService userService, IExternalLoginService externalLoginService, MembershipProviderBase usersMembershipProvider) { @@ -53,6 +55,7 @@ namespace Umbraco.Core.Security /// protected override void DisposeResources() { + _disposed = true; } /// @@ -62,6 +65,7 @@ namespace Umbraco.Core.Security /// public Task CreateAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); var userType = _userService.GetUserTypeByAlias( @@ -108,6 +112,7 @@ namespace Umbraco.Core.Security /// public async Task UpdateAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); var asInt = user.Id.TryConvertTo(); @@ -139,6 +144,7 @@ namespace Umbraco.Core.Security /// public Task DeleteAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); var asInt = user.Id.TryConvertTo(); @@ -164,6 +170,7 @@ namespace Umbraco.Core.Security /// public Task FindByIdAsync(int userId) { + ThrowIfDisposed(); var user = _userService.GetUserById(userId); if (user == null) { @@ -179,6 +186,7 @@ namespace Umbraco.Core.Security /// public Task FindByNameAsync(string userName) { + ThrowIfDisposed(); var user = _userService.GetByUsername(userName); if (user == null) { @@ -197,6 +205,7 @@ namespace Umbraco.Core.Security /// public Task SetPasswordHashAsync(BackOfficeIdentityUser user, string passwordHash) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (passwordHash.IsNullOrWhiteSpace()) throw new ArgumentNullException("passwordHash"); @@ -212,6 +221,7 @@ namespace Umbraco.Core.Security /// public Task GetPasswordHashAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.PasswordHash); @@ -224,6 +234,7 @@ namespace Umbraco.Core.Security /// public Task HasPasswordAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.PasswordHash.IsNullOrWhiteSpace() == false); @@ -236,6 +247,7 @@ namespace Umbraco.Core.Security /// public Task SetEmailAsync(BackOfficeIdentityUser user, string email) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (email.IsNullOrWhiteSpace()) throw new ArgumentNullException("email"); @@ -251,6 +263,7 @@ namespace Umbraco.Core.Security /// public Task GetEmailAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult(user.Email); @@ -263,6 +276,7 @@ namespace Umbraco.Core.Security /// public Task GetEmailConfirmedAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); throw new NotImplementedException(); } @@ -273,6 +287,7 @@ namespace Umbraco.Core.Security /// public Task SetEmailConfirmedAsync(BackOfficeIdentityUser user, bool confirmed) { + ThrowIfDisposed(); throw new NotImplementedException(); } @@ -283,6 +298,7 @@ namespace Umbraco.Core.Security /// public Task FindByEmailAsync(string email) { + ThrowIfDisposed(); var user = _userService.GetByEmail(email); var result = user == null ? null @@ -298,6 +314,7 @@ namespace Umbraco.Core.Security /// public Task AddLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (login == null) throw new ArgumentNullException("login"); @@ -316,6 +333,7 @@ namespace Umbraco.Core.Security /// public Task RemoveLoginAsync(BackOfficeIdentityUser user, UserLoginInfo login) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); if (login == null) throw new ArgumentNullException("login"); @@ -335,6 +353,7 @@ namespace Umbraco.Core.Security /// public Task> GetLoginsAsync(BackOfficeIdentityUser user) { + ThrowIfDisposed(); if (user == null) throw new ArgumentNullException("user"); return Task.FromResult((IList) user.Logins.Select(l => new UserLoginInfo(l.LoginProvider, l.ProviderKey)).ToList()); @@ -345,7 +364,10 @@ namespace Umbraco.Core.Security /// /// public Task FindAsync(UserLoginInfo login) - { + { + ThrowIfDisposed(); + if (login == null) throw new ArgumentNullException("login"); + //get all logins associated with the login id var result = _externalLoginService.Find(login).ToArray(); if (result.Any()) @@ -363,6 +385,117 @@ namespace Umbraco.Core.Security return Task.FromResult(null); } + + /// + /// Adds a user to a role (section) + /// + /// + /// + public Task AddToRoleAsync(BackOfficeIdentityUser user, string roleName) + { + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException("user"); + + if (user.AllowedSections.InvariantContains(roleName)) return Task.FromResult(0); + + var asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + } + + var found = _userService.GetUserById(asInt.Result); + + if (found != null) + { + found.AddAllowedSection(roleName); + } + + return Task.FromResult(0); + } + + /// + /// Removes the role (allowed section) for the user + /// + /// + /// + public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string roleName) + { + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException("user"); + + if (user.AllowedSections.InvariantContains(roleName) == false) return Task.FromResult(0); + + var asInt = user.Id.TryConvertTo(); + if (asInt == false) + { + throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); + } + + var found = _userService.GetUserById(asInt.Result); + + if (found != null) + { + found.RemoveAllowedSection(roleName); + } + + return Task.FromResult(0); + } + + /// + /// Returns the roles for this user + /// + /// + /// + public Task> GetRolesAsync(BackOfficeIdentityUser user) + { + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException("user"); + return Task.FromResult((IList)user.AllowedSections.ToList()); + } + + /// + /// Returns true if a user is in the role + /// + /// + /// + public Task IsInRoleAsync(BackOfficeIdentityUser user, string roleName) + { + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException("user"); + return Task.FromResult(user.AllowedSections.InvariantContains(roleName)); + } + + /// + /// Set the security stamp for the user + /// + /// + /// + public Task SetSecurityStampAsync(BackOfficeIdentityUser user, string stamp) + { + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException("user"); + + user.SecurityStamp = stamp; + return Task.FromResult(0); + } + + /// + /// Get the user security stamp + /// + /// + /// + public Task GetSecurityStampAsync(BackOfficeIdentityUser user) + { + ThrowIfDisposed(); + if (user == null) throw new ArgumentNullException("user"); + + //the stamp cannot be null, so if it is currently null then we'll just return a hash of the password + return Task.FromResult(user.SecurityStamp.IsNullOrWhiteSpace() + ? user.PasswordHash.ToMd5() + : user.SecurityStamp); + } + private BackOfficeIdentityUser AssignLoginsCallback(BackOfficeIdentityUser user) { if (user != null) @@ -424,6 +557,11 @@ namespace Umbraco.Core.Security anythingChanged = true; user.StartContentId = identityUser.StartContentId; } + if (user.SecurityStamp != identityUser.SecurityStamp) + { + anythingChanged = true; + user.SecurityStamp = identityUser.SecurityStamp; + } if (user.AllowedSections.ContainsAll(identityUser.AllowedSections) == false || identityUser.AllowedSections.ContainsAll(user.AllowedSections) == false) { @@ -441,76 +579,10 @@ namespace Umbraco.Core.Security return anythingChanged; } - /// - /// Adds a user to a role (section) - /// - /// - /// - public Task AddToRoleAsync(BackOfficeIdentityUser user, string roleName) + private void ThrowIfDisposed() { - if (user.AllowedSections.InvariantContains(roleName)) return Task.FromResult(0); - - var asInt = user.Id.TryConvertTo(); - if (asInt == false) - { - throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); - } - - var found = _userService.GetUserById(asInt.Result); - - if (found != null) - { - found.AddAllowedSection(roleName); - _userService.Save(found); - } - - return Task.FromResult(0); - } - - /// - /// Removes the role (allowed section) for the user - /// - /// - /// - public Task RemoveFromRoleAsync(BackOfficeIdentityUser user, string roleName) - { - if (user.AllowedSections.InvariantContains(roleName) == false) return Task.FromResult(0); - - var asInt = user.Id.TryConvertTo(); - if (asInt == false) - { - throw new InvalidOperationException("The user id must be an integer to work with the Umbraco"); - } - - var found = _userService.GetUserById(asInt.Result); - - if (found != null) - { - found.RemoveAllowedSection(roleName); - _userService.Save(found); - } - - return Task.FromResult(0); - } - - /// - /// Returns the roles for this user - /// - /// - /// - public Task> GetRolesAsync(BackOfficeIdentityUser user) - { - return Task.FromResult((IList)user.AllowedSections.ToList()); - } - - /// - /// Returns true if a user is in the role - /// - /// - /// - public Task IsInRoleAsync(BackOfficeIdentityUser user, string roleName) - { - return Task.FromResult(user.AllowedSections.InvariantContains(roleName)); + if (_disposed) + throw new ObjectDisposedException(GetType().Name); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c62c869e4e..61685d8fa3 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -381,6 +381,7 @@ + diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 0606cec1dd..9162aa801c 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -30,6 +30,7 @@ using umbraco.providers; using Microsoft.AspNet.Identity.Owin; using Newtonsoft.Json.Linq; using Umbraco.Core.Models.Identity; +using IUser = Umbraco.Core.Models.Membership.IUser; namespace Umbraco.Web.Editors { @@ -146,7 +147,7 @@ namespace Umbraco.Web.Editors ///
/// [SetAngularAntiForgeryTokens] - public HttpResponseMessage PostLogin(LoginModel loginModel) + public async Task PostLogin(LoginModel loginModel) { if (UmbracoContext.Security.ValidateBackOfficeCredentials(loginModel.Username, loginModel.Password)) { @@ -161,6 +162,10 @@ namespace Umbraco.Web.Editors // we cannot mix HttpContext.Response.Cookies and the way WebApi/Owin work) var ticket = response.UmbracoLoginWebApi(user); + //Identity does some of it's own checks as well so we need to use it's sign in process too... this will essentially re-create the + // ticket/cookie above but we need to create the ticket now so we can assign the Current Thread User/IPrinciple below + await SignInAsync(Mapper.Map(user), isPersistent: true); + var http = this.TryGetHttpContext(); if (http.Success == false) { @@ -211,15 +216,8 @@ namespace Umbraco.Web.Editors owinContext.Authentication.SignIn( new AuthenticationProperties() { IsPersistent = isPersistent }, - await GenerateUserIdentityAsync(user)); + await user.GenerateUserIdentityAsync(UserManager)); } - private async Task GenerateUserIdentityAsync(BackOfficeIdentityUser user) - { - // NOTE the authenticationType must match the umbraco one - // defined in CookieAuthenticationOptions.AuthenticationType - var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); - return userIdentity; - } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 336e1dbfde..3091f730a7 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -493,23 +493,14 @@ namespace Umbraco.Web.Editors OwinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); OwinContext.Authentication.SignIn( - new AuthenticationProperties() {IsPersistent = isPersistent}, - await GenerateUserIdentityAsync(user)); - } - - private async Task GenerateUserIdentityAsync(BackOfficeIdentityUser user) - { - // NOTE the authenticationType must match the umbraco one - // defined in CookieAuthenticationOptions.AuthenticationType - var userIdentity = await UserManager.CreateIdentityAsync(user, global::Umbraco.Core.Constants.Security.BackOfficeAuthenticationType); - return userIdentity; + new AuthenticationProperties() {IsPersistent = isPersistent}, + await user.GenerateUserIdentityAsync(UserManager)); } private IAuthenticationManager AuthenticationManager { get { return OwinContext.Authentication; } - } - + } /// /// Returns the server variables regarding the application state diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 7e70ba2958..cd0bccb7d6 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Web; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using Microsoft.Owin.Extensions; using Microsoft.Owin.Security; @@ -11,6 +13,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Logging; using Umbraco.Core.Models.Identity; using Umbraco.Core.Security; +using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Security.Identity { @@ -84,16 +87,14 @@ namespace Umbraco.Web.Security.Identity { Provider = new CookieAuthenticationProvider { - //TODO: Need to implement IUserSecurityStampStore on BackOfficeUserStore! - - //// 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 = SecurityStampValidator - // .OnValidateIdentity, UmbracoApplicationUser, int>( - // TimeSpan.FromMinutes(30), - // (manager, user) => user.GenerateUserIdentityAsync(manager), - // identity => identity.GetUserId()) + // 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 = SecurityStampValidator + .OnValidateIdentity( + TimeSpan.FromMinutes(30), + (manager, user) => user.GenerateUserIdentityAsync(manager), + identity => identity.GetUserId()) } }); From 86833aa8bf75e7d11ac7e755d4fae467092dd81f Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 12:21:41 +1100 Subject: [PATCH 25/38] Updates the back office external cookie name to be consistently cased with the other back office cookie names --- src/Umbraco.Core/Constants-Web.cs | 1 + src/Umbraco.Core/Security/AuthenticationExtensions.cs | 6 +++--- src/Umbraco.Web/Editors/BackOfficeController.cs | 4 ++-- src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs | 4 ++-- src/Umbraco.Web/Security/WebSecurity.cs | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 93f62130bd..ae80c70ecd 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -24,6 +24,7 @@ public const string BackOfficeAuthenticationType = "UmbracoBackOffice"; public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; + public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index ca597a8fef..9addb2e782 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -166,7 +166,7 @@ namespace Umbraco.Core.Security Path = "/" }; //remove the external login cookie too - var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalAuthenticationType, "") + var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalCookieName, "") { Expires = DateTime.Now.AddYears(-1), Path = "/" @@ -185,7 +185,7 @@ namespace Umbraco.Core.Security if (response == null) throw new ArgumentNullException("response"); //remove the external login cookie - var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalAuthenticationType, "") + var extLoginCookie = new CookieHeaderValue(Constants.Security.BackOfficeExternalCookieName, "") { Expires = DateTime.Now.AddYears(-1), Path = "/" @@ -352,7 +352,7 @@ namespace Umbraco.Core.Security { if (http == null) throw new ArgumentNullException("http"); //clear the preview cookie and external login - var cookies = new[] { cookieName, Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalAuthenticationType }; + var cookies = new[] { cookieName, Constants.Web.PreviewCookieName, Constants.Security.BackOfficeExternalCookieName }; foreach (var c in cookies) { //remove from the request diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 3091f730a7..258ca98b8d 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -479,9 +479,9 @@ namespace Umbraco.Web.Editors 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.BackOfficeExternalAuthenticationType] != null) + if (Response.Cookies[Core.Constants.Security.BackOfficeExternalCookieName] != null) { - Response.Cookies[Core.Constants.Security.BackOfficeExternalAuthenticationType].Expires = DateTime.MinValue; + Response.Cookies[Core.Constants.Security.BackOfficeExternalCookieName].Expires = DateTime.MinValue; } } diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index cd0bccb7d6..b8575e6875 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -111,12 +111,12 @@ namespace Umbraco.Web.Security.Identity { if (app == null) throw new ArgumentNullException("app"); - app.SetDefaultSignInAsAuthenticationType("UmbracoExternalCookie"); + app.SetDefaultSignInAsAuthenticationType(Constants.Security.BackOfficeExternalAuthenticationType); app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, AuthenticationMode = AuthenticationMode.Passive, - CookieName = Constants.Security.BackOfficeExternalAuthenticationType, + CookieName = Constants.Security.BackOfficeExternalCookieName, ExpireTimeSpan = TimeSpan.FromMinutes(5), //Custom cookie manager so we can filter requests CookieManager = new BackOfficeCookieManager(new SingletonUmbracoContextAccessor()), diff --git a/src/Umbraco.Web/Security/WebSecurity.cs b/src/Umbraco.Web/Security/WebSecurity.cs index 19857ddbcf..463b2c84e3 100644 --- a/src/Umbraco.Web/Security/WebSecurity.cs +++ b/src/Umbraco.Web/Security/WebSecurity.cs @@ -99,7 +99,7 @@ namespace Umbraco.Web.Security // and cookie handling is different with httpcontext vs webapi and owin, normally we'd do: //_httpContext.GetOwinContext().Authentication.SignOut(Constants.Security.BackOfficeExternalAuthenticationType); - var externalLoginCookie = _httpContext.Request.Cookies.Get(Constants.Security.BackOfficeExternalAuthenticationType); + var externalLoginCookie = _httpContext.Request.Cookies.Get(Constants.Security.BackOfficeExternalCookieName); if (externalLoginCookie != null) { externalLoginCookie.Expires = DateTime.Now.AddYears(-1); From b67250c3d5d983f74bfb8ae0836f6d034db728c1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 12:34:43 +1100 Subject: [PATCH 26/38] Updates back office auth extension methods to be explicit with specifying that it's for back office because we need to explicitly tell each provider the SignInAsAuthenticationType so that it uses the back office auth provider and not a user's own front-end one. --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/App_Code/OwinStartup.cs | 8 +++---- ....cs => UmbracoBackOfficeAuthExtensions.cs} | 21 ++++++++++++------- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 4 files changed, 21 insertions(+), 16 deletions(-) rename src/Umbraco.Web.UI/App_Code/{IdentityAuthExtensions.cs => UmbracoBackOfficeAuthExtensions.cs} (91%) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 9fd1385311..6c766f8e97 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.2.5")] -[assembly: AssemblyInformationalVersion("7.2.5")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.3.0")] +[assembly: AssemblyInformationalVersion("7.3.0")] \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs index 6d35ec5975..c54db8d53b 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Code/OwinStartup.cs @@ -47,10 +47,10 @@ namespace Umbraco.Web.UI * methods to suit your needs. */ - //app.ConfigureGoogleAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); - //app.ConfigureFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); - //app.ConfigureMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); - //app.ConfigureActiveDirectory("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); + //app.ConfigureBackOfficeGoogleAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); + //app.ConfigureBackOfficeFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); + //app.ConfigureBackOfficeMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); + //app.ConfigureBackOfficeActiveDirectoryAuth("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); } } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs b/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs similarity index 91% rename from src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs rename to src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs index c3aaf9345d..0c3afa36ad 100644 --- a/src/Umbraco.Web.UI/App_Code/IdentityAuthExtensions.cs +++ b/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs @@ -16,7 +16,7 @@ using Umbraco.Core; namespace Umbraco.Web.UI { - public static class IdentityAuthExtensions + public static class UmbracoBackOfficeAuthExtensions { /* @@ -42,12 +42,13 @@ namespace Umbraco.Web.UI /// http://go.microsoft.com/fwlink/?LinkID=144070 /// /// - public static void ConfigureMicrosoftAuth(this IAppBuilder app, string clientId, string clientSecret) + public static void ConfigureBackOfficeMicrosoftAuth(this IAppBuilder app, string clientId, string clientSecret) { var msOptions = new MicrosoftAccountAuthenticationOptions { ClientId = clientId, - ClientSecret = clientSecret + ClientSecret = clientSecret, + SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; //Defines styles for buttons msOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; @@ -59,7 +60,7 @@ namespace Umbraco.Web.UI */ /* - + /// /// Configure google sign-in /// @@ -80,12 +81,13 @@ namespace Umbraco.Web.UI /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials /// /// - public static void ConfigureGoogleAuth(this IAppBuilder app, string clientId, string clientSecret) + public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret) { var googleOptions = new GoogleOAuth2AuthenticationOptions { ClientId = clientId, - ClientSecret = clientSecret + ClientSecret = clientSecret, + SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; //Defines styles for buttons googleOptions.Description.Properties["SocialStyle"] = "btn-google-plus"; @@ -96,6 +98,7 @@ namespace Umbraco.Web.UI */ + /* /// @@ -118,12 +121,13 @@ namespace Umbraco.Web.UI /// https://developers.facebook.com/ /// /// - public static void ConfigureFacebookAuth(this IAppBuilder app, string appId, string appSecret) + public static void ConfigureBackOfficeFacebookAuth(this IAppBuilder app, string appId, string appSecret) { var fbOptions = new FacebookAuthenticationOptions { AppId = appId, AppSecret = appSecret, + SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; //Defines styles for buttons fbOptions.Description.Properties["SocialStyle"] = "btn-facebook"; @@ -167,7 +171,7 @@ namespace Umbraco.Web.UI /// This configuration requires the NaiveSessionCache class below which will need to be un-commented /// /// - public static void ConfigureActiveDirectory(this IAppBuilder app, + public static void ConfigureBackOfficeActiveDirectoryAuth(this IAppBuilder app, string tenant, string clientId, string postLoginRedirectUri, string appKey, string authType) { @@ -178,6 +182,7 @@ namespace Umbraco.Web.UI var adOptions = new OpenIdConnectAuthenticationOptions { AuthenticationType = authType, + SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, ClientId = clientId, Authority = authority, PostLogoutRedirectUri = postLoginRedirectUri, diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 3d58ed19bd..4d5120ff2b 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -356,7 +356,7 @@ Properties\SolutionInfo.cs - + loadStarterKits.ascx ASPXCodeBehind @@ -2586,7 +2586,7 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True 7300 / - http://localhost:7301 + http://localhost:7300 False False From bf59510c68a5153ed9a60827951ee1a068853bc0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 12:36:07 +1100 Subject: [PATCH 27/38] Removes setting the default sign in auth type - this is a user setting, we cannot modify that. --- src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index b8575e6875..bd8e47b6b3 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -111,7 +111,6 @@ namespace Umbraco.Web.Security.Identity { if (app == null) throw new ArgumentNullException("app"); - app.SetDefaultSignInAsAuthenticationType(Constants.Security.BackOfficeExternalAuthenticationType); app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, From 880c9cf679796b586384da8637020f2cad56be49 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 15:29:34 +1100 Subject: [PATCH 28/38] Updates back office extensions to use AuthenticationDescriptionOptionsExtension to configure the options for umb back office --- .../UmbracoBackOfficeAuthExtensions.cs | 233 +++++++++--------- ...henticationDescriptionOptionsExtensions.cs | 22 ++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 3 files changed, 140 insertions(+), 116 deletions(-) create mode 100644 src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs diff --git a/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs b/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs index 0c3afa36ad..59dea32f29 100644 --- a/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs +++ b/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs @@ -8,41 +8,45 @@ using System.Web; using Microsoft.Owin; using Owin; using Umbraco.Core; -//using Microsoft.Owin.Security.MicrosoftAccount; -//using Microsoft.IdentityModel.Clients.ActiveDirectory; +using Umbraco.Web.Security.Identity; //using Microsoft.Owin.Security.Facebook; //using Microsoft.Owin.Security.Google; //using Microsoft.Owin.Security.OpenIdConnect; +//using Microsoft.Owin.Security.MicrosoftAccount; +//using Microsoft.IdentityModel.Clients.ActiveDirectory; namespace Umbraco.Web.UI { public static class UmbracoBackOfficeAuthExtensions { - /* - /// - /// Configure microsoft account sign-in - /// - /// - /// - /// + /// + /// Configure microsoft account sign-in + /// + /// + /// + /// + /// + /// + /// /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.MicrosoftAccount /// - /// Nuget installation: - /// Microsoft.Owin.Security.MicrosoftAccount - /// - /// Microsoft account documentation for ASP.Net Identity can be found: - /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#MICROSOFT - /// http://blogs.msdn.com/b/webdev/archive/2012/09/19/configuring-your-asp-net-application-for-microsoft-oauth-account.aspx - /// - /// Microsoft apps can be created here: - /// - /// http://go.microsoft.com/fwlink/?LinkID=144070 - /// - /// - public static void ConfigureBackOfficeMicrosoftAuth(this IAppBuilder app, string clientId, string clientSecret) + /// Microsoft account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#MICROSOFT + /// http://blogs.msdn.com/b/webdev/archive/2012/09/19/configuring-your-asp-net-application-for-microsoft-oauth-account.aspx + /// + /// Microsoft apps can be created here: + /// + /// http://go.microsoft.com/fwlink/?LinkID=144070 + /// + /// + public static void ConfigureBackOfficeMicrosoftAuth(this IAppBuilder app, string clientId, string clientSecret, + string caption = "Microsoft", string style = "btn-microsoft", string icon = "fa-windows") { var msOptions = new MicrosoftAccountAuthenticationOptions { @@ -50,38 +54,37 @@ namespace Umbraco.Web.UI ClientSecret = clientSecret, SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; - //Defines styles for buttons - msOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; - msOptions.Description.Properties["SocialIcon"] = "fa-windows"; - msOptions.Caption = "Microsoft"; + msOptions.Description.ForUmbracoBackOffice(style, icon); + msOptions.Caption = caption; app.UseMicrosoftAccountAuthentication(msOptions); } - */ - /* - - /// - /// Configure google sign-in - /// - /// - /// - /// + /// + /// Configure google sign-in + /// + /// + /// + /// + /// + /// + /// /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.Google /// - /// Nuget installation: - /// Microsoft.Owin.Security.Google - /// - /// Google account documentation for ASP.Net Identity can be found: - /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE - /// - /// Google apps can be created here: - /// - /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials - /// - /// - public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret) + /// Google account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE + /// + /// Google apps can be created here: + /// + /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials + /// + /// + public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret, + string caption = "Google", string style = "btn-google-plus", string icon = "fa-google-plus") { var googleOptions = new GoogleOAuth2AuthenticationOptions { @@ -89,39 +92,37 @@ namespace Umbraco.Web.UI ClientSecret = clientSecret, SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; - //Defines styles for buttons - googleOptions.Description.Properties["SocialStyle"] = "btn-google-plus"; - googleOptions.Description.Properties["SocialIcon"] = "fa-google-plus"; - googleOptions.Caption = "Google"; + googleOptions.Description.ForUmbracoBackOffice(style, icon); + googleOptions.Caption = caption; app.UseGoogleAuthentication(googleOptions); } - */ - - /* - - /// - /// Configure facebook sign-in - /// - /// - /// - /// + /// + /// Configure facebook sign-in + /// + /// + /// + /// + /// + /// + /// /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.Facebook /// - /// Nuget installation: - /// Microsoft.Owin.Security.Facebook - /// - /// Facebook account documentation for ASP.Net Identity can be found: + /// Facebook account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#FACEBOOK + /// + /// Facebook apps can be created here: /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#FACEBOOK - /// - /// Facebook apps can be created here: - /// - /// https://developers.facebook.com/ - /// - /// - public static void ConfigureBackOfficeFacebookAuth(this IAppBuilder app, string appId, string appSecret) + /// https://developers.facebook.com/ + /// + /// + public static void ConfigureBackOfficeFacebookAuth(this IAppBuilder app, string appId, string appSecret, + string caption = "Facebook", string style = "btn-facebook", string icon = "fa-facebook") { var fbOptions = new FacebookAuthenticationOptions { @@ -129,51 +130,49 @@ namespace Umbraco.Web.UI AppSecret = appSecret, SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; - //Defines styles for buttons - fbOptions.Description.Properties["SocialStyle"] = "btn-facebook"; - fbOptions.Description.Properties["SocialIcon"] = "fa-facebook"; - fbOptions.Caption = "Facebook"; + fbOptions.Description.ForUmbracoBackOffice(style, icon); + fbOptions.Caption = caption; app.UseFacebookAuthentication(fbOptions); } - - */ - /* - - /// - /// Configure ActiveDirectory sign-in - /// - /// - /// - /// - /// - /// The URL that will be redirected to after login is successful, example: http://mydomain.com/umbraco/; - /// - /// - /// - /// This by default is 'OpenIdConnect' but that doesn't match what ASP.Net Identity actually stores in the - /// loginProvider field in the database which looks something like this (for example): - /// https://sts.windows.net/3bb0b4c5-364f-4394-ad36-0f29f95e5ggg/ - /// and is based on your AD setup. This value needs to match in order for accounts to - /// detected as linked/un-linked in the back office. - /// + /// + /// Configure ActiveDirectory sign-in + /// + /// + /// + /// + /// + /// The URL that will be redirected to after login is successful, example: http://mydomain.com/umbraco/; + /// + /// + /// + /// This by default is 'OpenIdConnect' but that doesn't match what ASP.Net Identity actually stores in the + /// loginProvider field in the database which looks something like this (for example): + /// https://sts.windows.net/3bb0b4c5-364f-4394-ad36-0f29f95e5ggg/ + /// and is based on your AD setup. This value needs to match in order for accounts to + /// detected as linked/un-linked in the back office. + /// + /// + /// + /// /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.OpenIdConnect + /// Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory /// - /// Nuget installation: - /// Microsoft.Owin.Security.OpenIdConnect - /// Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory - /// - /// ActiveDirectory account documentation for ASP.Net Identity can be found: + /// ActiveDirectory account documentation for ASP.Net Identity can be found: + /// + /// https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet /// - /// https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet - /// - /// This configuration requires the NaiveSessionCache class below which will need to be un-commented - /// - /// + /// This configuration requires the NaiveSessionCache class below which will need to be un-commented + /// + /// public static void ConfigureBackOfficeActiveDirectoryAuth(this IAppBuilder app, string tenant, string clientId, string postLoginRedirectUri, string appKey, - string authType) + string authType, + string caption = "Active Directory", string style = "btn-microsoft", string icon = "fa-windows") { const string aadInstance = "https://login.windows.net/{0}"; const string graphResourceId = "https://graph.windows.net"; @@ -209,14 +208,16 @@ namespace Umbraco.Web.UI } }; - adOptions.Description.Properties["SocialStyle"] = "btn-microsoft"; - adOptions.Description.Properties["SocialIcon"] = "fa-windows"; - adOptions.Caption = "Active Directory"; + adOptions.Description.ForUmbracoBackOffice(style, icon); + adOptions.Caption = caption; app.UseOpenIdConnectAuthentication(adOptions); } - - */ + + */ + } + + /* diff --git a/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs b/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs new file mode 100644 index 0000000000..07ed17f423 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.Owin.Security; + +namespace Umbraco.Web.Security.Identity +{ + public static class AuthenticationDescriptionOptionsExtensions + { + /// + /// Configures the properties of the authentication description instance for use with Umbraco back office + /// + /// + /// + /// + public static void ForUmbracoBackOffice(this AuthenticationDescription options, string style, string icon) + { + options.Properties["SocialStyle"] = style; + options.Properties["SocialIcon"] = icon; + + //flag for use in back office + options.Properties["UmbracoBackOffice"] = true; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0b8fea4a2f..72b3cd0262 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -551,6 +551,7 @@ + From abf70cd3020523169d889bd0739ee043e117b7d7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 15:35:03 +1100 Subject: [PATCH 29/38] filters external login providers in the back office to only show the ones configured for umbraco back office --- src/Umbraco.Web.UI/umbraco/Views/Default.cshtml | 2 +- src/Umbraco.Web/Editors/BackOfficeController.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml index a392942fe1..b37c14b421 100644 --- a/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml +++ b/src/Umbraco.Web.UI/umbraco/Views/Default.cshtml @@ -58,11 +58,11 @@ @{ var loginProviders = Context.GetOwinContext().Authentication.GetExternalAuthenticationTypes() + .Where(p => p.Properties.ContainsKey("UmbracoBackOffice")) .Select(p => new { authType = p.AuthenticationType, caption = p.Caption, - //TODO: Need to see if this exposes any sensitive data! properties = p.Properties }) .ToArray(); diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 258ca98b8d..3a6f91eb2c 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -395,6 +395,7 @@ namespace Umbraco.Web.Editors { { "providers", HttpContext.GetOwinContext().Authentication.GetExternalAuthenticationTypes() + .Where(p => p.Properties.ContainsKey("UmbracoBackOffice")) .Select(p => new { authType = p.AuthenticationType, caption = p.Caption, From 349cb91e3a4d37f4ab9648373ca095f2ec5a9431 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 15:48:57 +1100 Subject: [PATCH 30/38] updates expires logic --- src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs b/src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs index 448e04222c..c266b1ed5e 100644 --- a/src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs +++ b/src/Umbraco.Web/Mvc/DisableClientCacheAttribute.cs @@ -13,7 +13,8 @@ namespace Umbraco.Web.Mvc { if (filterContext.IsChildAction) base.OnResultExecuting(filterContext); - filterContext.HttpContext.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1)); + filterContext.HttpContext.Response.Cache.SetExpires(DateTime.Now.AddDays(-10)); + filterContext.HttpContext.Response.Cache.SetLastModified(DateTime.Now); filterContext.HttpContext.Response.Cache.SetValidUntilExpires(false); filterContext.HttpContext.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches); filterContext.HttpContext.Response.Cache.SetCacheability(HttpCacheability.NoCache); From 140d3c026876fa7f8806b0df25a8c5b7f05d0612 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 16:18:38 +1100 Subject: [PATCH 31/38] Update to latest Identity. Moves startup code to App_Start like VS templates have, this will be better for our nuget packages (like i do in UmbracoIdentity which works for both web apps and websites). Updates web.config to explicitly declare the owin startup otherwise we'll end up with conflicts (YSODs) and now people can configure it properly. --- src/Umbraco.Core/Umbraco.Core.csproj | 10 ++- src/Umbraco.Core/packages.config | 4 +- .../{App_Code => App_Start}/OwinStartup.cs | 6 +- .../UmbracoBackOfficeAuthExtensions.cs | 74 ++++++++++--------- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 13 ++-- src/Umbraco.Web.UI/packages.config | 4 +- src/Umbraco.Web.UI/web.Template.Debug.config | 3 +- src/Umbraco.Web.UI/web.Template.config | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 10 ++- src/Umbraco.Web/packages.config | 4 +- 10 files changed, 70 insertions(+), 60 deletions(-) rename src/Umbraco.Web.UI/{App_Code => App_Start}/OwinStartup.cs (89%) rename src/Umbraco.Web.UI/{App_Code => App_Start}/UmbracoBackOfficeAuthExtensions.cs (86%) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 61685d8fa3..523fde9136 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -54,11 +54,13 @@ False ..\packages\log4net-mediumtrust.2.0.0\lib\log4net.dll - - ..\packages\Microsoft.AspNet.Identity.Core.2.1.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + + False + ..\packages\Microsoft.AspNet.Identity.Core.2.2.0\lib\net45\Microsoft.AspNet.Identity.Core.dll - - ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + False + ..\packages\Microsoft.AspNet.Identity.Owin.2.2.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll False diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index 6281d1338f..fd68072884 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -3,8 +3,8 @@ - - + + diff --git a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs b/src/Umbraco.Web.UI/App_Start/OwinStartup.cs similarity index 89% rename from src/Umbraco.Web.UI/App_Code/OwinStartup.cs rename to src/Umbraco.Web.UI/App_Start/OwinStartup.cs index c54db8d53b..9279750636 100644 --- a/src/Umbraco.Web.UI/App_Code/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Start/OwinStartup.cs @@ -5,12 +5,12 @@ using Umbraco.Core.Security; using Umbraco.Web.Security.Identity; using Umbraco.Web.UI; -[assembly: OwinStartup(typeof(OwinStartup))] +[assembly: OwinStartup("UmbracoStartup", typeof(OwinStartup))] namespace Umbraco.Web.UI { /// - /// Default OWIN startup class + /// Default OWIN startup class as specified in appSettings /// public class OwinStartup { @@ -47,7 +47,7 @@ namespace Umbraco.Web.UI * methods to suit your needs. */ - //app.ConfigureBackOfficeGoogleAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); + //app.ConfigureBackOfficeGoogleAuth("1072120697051-p41pro11srud3o3n90j7m00geq426jqt.apps.googleusercontent.com", "ak0msWvSE4w9nujcsfVy8_Y0"); //app.ConfigureBackOfficeFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); //app.ConfigureBackOfficeMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); //app.ConfigureBackOfficeActiveDirectoryAuth("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); diff --git a/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs b/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs similarity index 86% rename from src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs rename to src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs index 59dea32f29..180334e5a6 100644 --- a/src/Umbraco.Web.UI/App_Code/UmbracoBackOfficeAuthExtensions.cs +++ b/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs @@ -59,44 +59,46 @@ namespace Umbraco.Web.UI app.UseMicrosoftAccountAuthentication(msOptions); } + */ - /// - /// Configure google sign-in - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Nuget installation: - /// Microsoft.Owin.Security.Google - /// - /// Google account documentation for ASP.Net Identity can be found: - /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE - /// - /// Google apps can be created here: - /// - /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials - /// - /// - public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret, - string caption = "Google", string style = "btn-google-plus", string icon = "fa-google-plus") - { - var googleOptions = new GoogleOAuth2AuthenticationOptions - { - ClientId = clientId, - ClientSecret = clientSecret, - SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType - }; - googleOptions.Description.ForUmbracoBackOffice(style, icon); - googleOptions.Caption = caption; - app.UseGoogleAuthentication(googleOptions); - } + ///// + ///// Configure google sign-in + ///// + ///// + ///// + ///// + ///// + ///// + ///// + ///// + ///// + ///// Nuget installation: + ///// Microsoft.Owin.Security.Google + ///// + ///// Google account documentation for ASP.Net Identity can be found: + ///// + ///// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE + ///// + ///// Google apps can be created here: + ///// + ///// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials + ///// + ///// + //public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret, + // string caption = "Google", string style = "btn-google-plus", string icon = "fa-google-plus") + //{ + // var googleOptions = new GoogleOAuth2AuthenticationOptions + // { + // ClientId = clientId, + // ClientSecret = clientSecret, + // SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType + // }; + // googleOptions.Description.ForUmbracoBackOffice(style, icon); + // googleOptions.Caption = caption; + // app.UseGoogleAuthentication(googleOptions); + //} + /* /// /// Configure facebook sign-in diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 4d5120ff2b..bc2269b1b2 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -152,10 +152,12 @@ ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll - ..\packages\Microsoft.AspNet.Identity.Core.2.1.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + ..\packages\Microsoft.AspNet.Identity.Core.2.2.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + True - - ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + False + ..\packages\Microsoft.AspNet.Identity.Owin.2.2.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll @@ -355,8 +357,8 @@ Properties\SolutionInfo.cs - - + + loadStarterKits.ascx ASPXCodeBehind @@ -2551,6 +2553,7 @@ + diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index e5d15942f1..3de27f18a4 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -9,8 +9,8 @@ - - + + diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index 732e2e76ce..caa949f801 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -61,8 +61,9 @@ + - + diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index b769cc7318..5357158046 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -51,7 +51,7 @@ - + diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 72b3cd0262..45c4b51efb 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -131,11 +131,13 @@ False ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll - - ..\packages\Microsoft.AspNet.Identity.Core.2.1.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + + False + ..\packages\Microsoft.AspNet.Identity.Core.2.2.0\lib\net45\Microsoft.AspNet.Identity.Core.dll - - ..\packages\Microsoft.AspNet.Identity.Owin.2.1.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + + False + ..\packages\Microsoft.AspNet.Identity.Owin.2.2.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index bd20574668..0257d9b37a 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -6,8 +6,8 @@ - - + + From e46849206436196e60c9d7b9ef79b792b060e1ba Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 25 Mar 2015 19:01:29 +1100 Subject: [PATCH 32/38] Updates back office ext methods to include the CallbackPath which is key to make multi-tenanted work and ensures that the back office providers are linked with the umbraco back office external cookie provider. Adds some docs about it too. Updates the web.config templates to ensure the correct assembly redirects. --- src/Umbraco.Core/Constants-Web.cs | 9 ++ .../UmbracoBackOfficeAuthExtensions.cs | 96 ++++++++++--------- src/Umbraco.Web.UI/web.Template.Debug.config | 28 ++++++ ...henticationDescriptionOptionsExtensions.cs | 17 +++- 4 files changed, 101 insertions(+), 49 deletions(-) diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index ae80c70ecd..a936b2e388 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -26,6 +26,15 @@ public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; + /// + /// The prefix used for external identity providers for their authentication type + /// + /// + /// By default we don't want to interfere with front-end external providers and their default setup, for back office the + /// providers need to be setup differently and each auth type for the back office will be prefixed with this value + /// + public const string BackOfficeExternalAuthenticationTypePrefix = "Umbraco."; + public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; public const string StartMediaNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startmedianode"; public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapps"; diff --git a/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs b/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs index 180334e5a6..0090029bf4 100644 --- a/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs +++ b/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs @@ -54,51 +54,52 @@ namespace Umbraco.Web.UI ClientSecret = clientSecret, SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType }; - msOptions.Description.ForUmbracoBackOffice(style, icon); + msOptions.ForUmbracoBackOffice(style, icon); msOptions.Caption = caption; app.UseMicrosoftAccountAuthentication(msOptions); } - */ - - ///// - ///// Configure google sign-in - ///// - ///// - ///// - ///// - ///// - ///// - ///// - ///// - ///// - ///// Nuget installation: - ///// Microsoft.Owin.Security.Google - ///// - ///// Google account documentation for ASP.Net Identity can be found: - ///// - ///// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE - ///// - ///// Google apps can be created here: - ///// - ///// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials - ///// - ///// - //public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret, - // string caption = "Google", string style = "btn-google-plus", string icon = "fa-google-plus") - //{ - // var googleOptions = new GoogleOAuth2AuthenticationOptions - // { - // ClientId = clientId, - // ClientSecret = clientSecret, - // SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType - // }; - // googleOptions.Description.ForUmbracoBackOffice(style, icon); - // googleOptions.Caption = caption; - // app.UseGoogleAuthentication(googleOptions); - //} - - /* + /// + /// Configure google sign-in + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// Nuget installation: + /// Microsoft.Owin.Security.Google + /// + /// Google account documentation for ASP.Net Identity can be found: + /// + /// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE + /// + /// Google apps can be created here: + /// + /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials + /// + /// + public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret, + string caption = "Google", string style = "btn-google-plus", string icon = "fa-google-plus") + { + var googleOptions = new GoogleOAuth2AuthenticationOptions + { + ClientId = clientId, + ClientSecret = clientSecret, + //In order to allow using different google providers on the front-end vs the back office, + // these settings are very important to make them distinguished from one another. + SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, + // By default this is '/signin-google', you will need to change that default value in your + // Google developer settings for your web-app in the "REDIRECT URIS" setting + CallbackPath = new PathString("/umbraco-google-signin") + }; + googleOptions.ForUmbracoBackOffice(style, icon); + googleOptions.Caption = caption; + app.UseGoogleAuthentication(googleOptions); + } /// /// Configure facebook sign-in @@ -130,14 +131,19 @@ namespace Umbraco.Web.UI { AppId = appId, AppSecret = appSecret, - SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType + //In order to allow using different google providers on the front-end vs the back office, + // these settings are very important to make them distinguished from one another. + SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, + // By default this is '/signin-facebook', you will need to change that default value in your + // Facebook developer settings for your app in the Advanced settings under "Client OAuth Login" + // in the "Valid OAuth redirect URIs", specify the full URL, for example: http://mysite.com/umbraco-facebook-signin + CallbackPath = new PathString("/umbraco-facebook-signin") }; - fbOptions.Description.ForUmbracoBackOffice(style, icon); + fbOptions.ForUmbracoBackOffice(style, icon); fbOptions.Caption = caption; app.UseFacebookAuthentication(fbOptions); } - /// /// Configure ActiveDirectory sign-in /// @@ -210,7 +216,7 @@ namespace Umbraco.Web.UI } }; - adOptions.Description.ForUmbracoBackOffice(style, icon); + adOptions.ForUmbracoBackOffice(style, icon); adOptions.Caption = caption; app.UseOpenIdConnectAuthentication(adOptions); } diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index caa949f801..476d664dc5 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -172,6 +172,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs b/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs index 07ed17f423..47ad1b4310 100644 --- a/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AuthenticationDescriptionOptionsExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Owin.Security; +using Umbraco.Core; namespace Umbraco.Web.Security.Identity { @@ -10,13 +11,21 @@ namespace Umbraco.Web.Security.Identity /// /// /// - public static void ForUmbracoBackOffice(this AuthenticationDescription options, string style, string icon) + public static void ForUmbracoBackOffice(this AuthenticationOptions options, string style, string icon) { - options.Properties["SocialStyle"] = style; - options.Properties["SocialIcon"] = icon; + Mandate.ParameterNotNullOrEmpty(options.AuthenticationType, "options.AuthenticationType"); + + //Ensure the prefix is set + if (options.AuthenticationType.StartsWith(Constants.Security.BackOfficeExternalAuthenticationTypePrefix) == false) + { + options.AuthenticationType = Constants.Security.BackOfficeExternalAuthenticationTypePrefix + options.AuthenticationType; + } + + options.Description.Properties["SocialStyle"] = style; + options.Description.Properties["SocialIcon"] = icon; //flag for use in back office - options.Properties["UmbracoBackOffice"] = true; + options.Description.Properties["UmbracoBackOffice"] = true; } } } \ No newline at end of file From 6efd14eff32d548915fd288f7b31ad514ec30b54 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 26 Mar 2015 17:43:22 +1100 Subject: [PATCH 33/38] Updates the startup auth code extension methods to better support extensibility so people could override the default user store or manager in order to implement some interfaces that we currently don't. --- .../Models/Identity/BackOfficeIdentityUser.cs | 2 +- .../Models/Identity/IdentityUser.cs | 23 +++--- .../Security/BackOfficeUserManager.cs | 82 ++++++++++--------- .../Editors/BackOfficeController.cs | 6 ++ .../Security/Identity/AppBuilderExtensions.cs | 32 ++++++++ 5 files changed, 97 insertions(+), 48 deletions(-) diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 31c56ba013..1523cf9040 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Models.Identity public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim> { - public async Task GenerateUserIdentityAsync(BackOfficeUserManager manager) + public virtual async Task GenerateUserIdentityAsync(BackOfficeUserManager manager) { // NOTE the authenticationType must match the umbraco one // defined in CookieAuthenticationOptions.AuthenticationType diff --git a/src/Umbraco.Core/Models/Identity/IdentityUser.cs b/src/Umbraco.Core/Models/Identity/IdentityUser.cs index 09306bb1f0..cba4fc514a 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityUser.cs @@ -18,6 +18,18 @@ namespace Umbraco.Core.Models.Identity where TRole : IdentityUserRole where TClaim : IdentityUserClaim { + + /// + /// Constructor + /// + /// + public IdentityUser() + { + this.Claims = new List(); + this.Roles = new List(); + this.Logins = new List(); + } + /// /// Email /// @@ -108,15 +120,6 @@ namespace Umbraco.Core.Models.Identity /// public virtual string UserName { get; set; } - /// - /// Constructor - /// - /// - public IdentityUser() - { - this.Claims = new List(); - this.Roles = new List(); - this.Logins = new List(); - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index b410f34107..def46b7556 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -13,47 +13,14 @@ using Umbraco.Core.Services; namespace Umbraco.Core.Security { /// - /// Back office user manager + /// Default back office user manager /// - public class BackOfficeUserManager : UserManager + public class BackOfficeUserManager : BackOfficeUserManager { public BackOfficeUserManager(IUserStore store) : base(store) { } - - #region What we support do not currently - - //NOTE: Not sure if we really want/need to ever support this - public override bool SupportsUserClaim - { - get { return false; } - } - - //TODO: Support this - public override bool SupportsQueryableUsers - { - get { return false; } - } - - //TODO: Support this - public override bool SupportsUserLockout - { - get { return false; } - } - - //TODO: Support this - public override bool SupportsUserTwoFactor - { - get { return false; } - } - - //TODO: Support this - public override bool SupportsUserPhoneNumber - { - get { return false; } - } - #endregion /// /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager @@ -155,10 +122,51 @@ namespace Umbraco.Core.Security return manager; } + } - protected override void Dispose(bool disposing) + /// + /// Generic Back office user manager + /// + public class BackOfficeUserManager : UserManager + where T : BackOfficeIdentityUser + { + public BackOfficeUserManager(IUserStore store) + : base(store) { - base.Dispose(disposing); } + + #region What we support do not currently + + //NOTE: Not sure if we really want/need to ever support this + public override bool SupportsUserClaim + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsQueryableUsers + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsUserLockout + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsUserTwoFactor + { + get { return false; } + } + + //TODO: Support this + public override bool SupportsUserPhoneNumber + { + get { return false; } + } + #endregion + } } diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 3a6f91eb2c..40ca7d428a 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -472,6 +472,12 @@ namespace Umbraco.Web.Editors var user = await UserManager.FindAsync(loginInfo.Login); if (user != null) { + //TODO: It might be worth keeping some of the claims associated with the ExternalLoginInfo, in which case we + // wouldn't necessarily sign the user in here with the standard login, instead we'd update the + // UseUmbracoBackOfficeExternalCookieAuthentication extension method to have the correct provider and claims factory, + // 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 SignInAsync(user, isPersistent: false); } diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index bd8e47b6b3..c1cff6a5aa 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using System.Web; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; @@ -31,6 +32,9 @@ namespace Umbraco.Web.Security.Identity ApplicationContext appContext, MembershipProviderBase userMembershipProvider) { + if (appContext == null) throw new ArgumentNullException("appContext"); + if (userMembershipProvider == null) throw new ArgumentNullException("userMembershipProvider"); + //Don't proceed if the app is not ready if (appContext.IsConfigured == false || appContext.DatabaseContext == null @@ -57,6 +61,10 @@ namespace Umbraco.Web.Security.Identity MembershipProviderBase userMembershipProvider, BackOfficeUserStore customUserStore) { + if (appContext == null) throw new ArgumentNullException("appContext"); + if (userMembershipProvider == null) throw new ArgumentNullException("userMembershipProvider"); + if (customUserStore == null) throw new ArgumentNullException("customUserStore"); + //Don't proceed if the app is not ready if (appContext.IsConfigured == false || appContext.DatabaseContext == null @@ -70,6 +78,30 @@ namespace Umbraco.Web.Security.Identity userMembershipProvider)); } + /// + /// Configure a custom BackOfficeUserManager for Umbraco + /// + /// + /// + /// + public static void ConfigureUserManagerForUmbracoBackOffice(this IAppBuilder app, + ApplicationContext appContext, + Func, IOwinContext, TManager> userManager) + where TManager : BackOfficeUserManager + where TUser : BackOfficeIdentityUser + { + if (appContext == null) throw new ArgumentNullException("appContext"); + if (userManager == null) throw new ArgumentNullException("userManager"); + + //Don't proceed if the app is not ready + if (appContext.IsConfigured == false + || appContext.DatabaseContext == null + || appContext.DatabaseContext.IsDatabaseConfigured == false) return; + + //Configure Umbraco user manager to be created per request + app.CreatePerOwinContext(userManager); + } + /// /// Ensures that the UmbracoBackOfficeAuthenticationMiddleware is assigned to the pipeline /// From 394cab5ab4e3288053252630e37db1680f993a28 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 26 Mar 2015 18:09:48 +1100 Subject: [PATCH 34/38] Updates the owin startup classes - we now have a default one shipped as a DLL which will always execute based on the appSettings, then we can ship with 2 optional ones that people can learn and use from which just requires them to update the appSetting. Now to decide on how to ship these .cs files --- ...Startup.cs => CustomUmbracoOwinStartup.cs} | 18 ++++--- .../App_Start/StandardUmbracoOwinStartup.cs | 50 +++++++++++++++++++ src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 3 +- src/Umbraco.Web.UI/web.Template.Debug.config | 2 +- src/Umbraco.Web.UI/web.Template.config | 2 +- src/Umbraco.Web/DefaultUmbracoOwinStartup.cs | 35 +++++++++++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + 7 files changed, 101 insertions(+), 10 deletions(-) rename src/Umbraco.Web.UI/App_Start/{OwinStartup.cs => CustomUmbracoOwinStartup.cs} (78%) create mode 100644 src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs create mode 100644 src/Umbraco.Web/DefaultUmbracoOwinStartup.cs diff --git a/src/Umbraco.Web.UI/App_Start/OwinStartup.cs b/src/Umbraco.Web.UI/App_Start/CustomUmbracoOwinStartup.cs similarity index 78% rename from src/Umbraco.Web.UI/App_Start/OwinStartup.cs rename to src/Umbraco.Web.UI/App_Start/CustomUmbracoOwinStartup.cs index 9279750636..6769b760a9 100644 --- a/src/Umbraco.Web.UI/App_Start/OwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Start/CustomUmbracoOwinStartup.cs @@ -5,14 +5,19 @@ using Umbraco.Core.Security; using Umbraco.Web.Security.Identity; using Umbraco.Web.UI; -[assembly: OwinStartup("UmbracoStartup", typeof(OwinStartup))] +[assembly: OwinStartup("CustomUmbracoStartup", typeof(StandardUmbracoOwinStartup))] namespace Umbraco.Web.UI { /// - /// Default OWIN startup class as specified in appSettings + /// A custom way to configure OWIN for Umbraco /// - public class OwinStartup + /// + /// The startup type is specified in appSettings under owin:appStartup - change it to "CustomUmbracoStartup" to use this class + /// + /// This startup class would allow you to customize the Identity IUserStore and/or IUserManager for the Umbraco Backoffice + /// + public class CustomUmbracoOwinStartup { public void Configuration(IAppBuilder app) { @@ -22,14 +27,13 @@ namespace Umbraco.Web.UI ApplicationContext.Current, Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); - //Ensure owin is configured for Umbraco back office authentication. If you have any front-end OWIN - // cookie configuration, this must be declared after it. + //Ensure owin is configured for Umbraco back office authentication app .UseUmbracoBackOfficeCookieAuthentication() .UseUmbracoBackOfficeExternalCookieAuthentication(); /* - * Configure external logins: + * Configure external logins for the back office: * * Depending on the authentication sources you would like to enable, you will need to install * certain Nuget packages. @@ -47,7 +51,7 @@ namespace Umbraco.Web.UI * methods to suit your needs. */ - //app.ConfigureBackOfficeGoogleAuth("1072120697051-p41pro11srud3o3n90j7m00geq426jqt.apps.googleusercontent.com", "ak0msWvSE4w9nujcsfVy8_Y0"); + //app.ConfigureBackOfficeGoogleAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); //app.ConfigureBackOfficeFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); //app.ConfigureBackOfficeMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); //app.ConfigureBackOfficeActiveDirectoryAuth("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); diff --git a/src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs b/src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs new file mode 100644 index 0000000000..a684efa2be --- /dev/null +++ b/src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs @@ -0,0 +1,50 @@ +using Microsoft.Owin; +using Owin; +using Umbraco.Core; +using Umbraco.Core.Security; +using Umbraco.Web.Security.Identity; +using Umbraco.Web.UI; + +[assembly: OwinStartup("StandardUmbracoStartup", typeof(StandardUmbracoOwinStartup))] + +namespace Umbraco.Web.UI +{ + /// + /// The standard way to configure OWIN for Umbraco + /// + /// + /// The startup type is specified in appSettings under owin:appStartup - change it to "StandardUmbracoStartup" to use this class + /// + public class StandardUmbracoOwinStartup : DefaultUmbracoOwinStartup + { + public override void Configuration(IAppBuilder app) + { + //ensure the default options are configured + base.Configuration(app); + + /* + * Configure external logins for the back office: + * + * Depending on the authentication sources you would like to enable, you will need to install + * certain Nuget packages. + * + * For Google auth: Install-Package Microsoft.Owin.Security.Google + * For Facebook auth: Install-Package Microsoft.Owin.Security.Facebook + * For Microsoft auth: Install-Package Microsoft.Owin.Security.MicrosoftAccount + * + * There are many more providers such as Twitter, Yahoo, ActiveDirectory, etc... most information can + * be found here: http://www.asp.net/web-api/overview/security/external-authentication-services + * + * The source for these methods is located in ~/App_Code/IdentityAuthExtensions.cs, you will need to un-comment + * the methods that you would like to use. Each method contains documentation and links to + * documentation for reference. You can also tweak the code in those extension + * methods to suit your needs. + */ + + //app.ConfigureBackOfficeGoogleAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); + //app.ConfigureBackOfficeFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); + //app.ConfigureBackOfficeMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); + //app.ConfigureBackOfficeActiveDirectoryAuth("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index bc2269b1b2..ee56ec0fef 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -357,7 +357,8 @@ Properties\SolutionInfo.cs - + + loadStarterKits.ascx diff --git a/src/Umbraco.Web.UI/web.Template.Debug.config b/src/Umbraco.Web.UI/web.Template.Debug.config index 476d664dc5..58c400efb3 100644 --- a/src/Umbraco.Web.UI/web.Template.Debug.config +++ b/src/Umbraco.Web.UI/web.Template.Debug.config @@ -61,7 +61,7 @@ - + diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index 5357158046..825f2253d4 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -51,7 +51,7 @@ - + diff --git a/src/Umbraco.Web/DefaultUmbracoOwinStartup.cs b/src/Umbraco.Web/DefaultUmbracoOwinStartup.cs new file mode 100644 index 0000000000..16c2140a91 --- /dev/null +++ b/src/Umbraco.Web/DefaultUmbracoOwinStartup.cs @@ -0,0 +1,35 @@ +using Microsoft.Owin; +using Owin; +using Umbraco.Core; +using Umbraco.Core.Security; +using Umbraco.Web; +using Umbraco.Web.Security.Identity; + +[assembly: OwinStartup("DefaultUmbracoStartup", typeof(DefaultUmbracoOwinStartup))] + +namespace Umbraco.Web +{ + /// + /// The default way to configure OWIN for Umbraco + /// + /// + /// The startup type is specified in appSettings under owin:appStartup + /// + public class DefaultUmbracoOwinStartup + { + public virtual void Configuration(IAppBuilder app) + { + //Configure the Identity user manager for use with Umbraco Back office + // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) + app.ConfigureUserManagerForUmbracoBackOffice( + ApplicationContext.Current, + Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); + + //Ensure owin is configured for Umbraco back office authentication. If you have any front-end OWIN + // cookie configuration, this must be declared after it. + app + .UseUmbracoBackOfficeCookieAuthentication() + .UseUmbracoBackOfficeExternalCookieAuthentication(); + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 45c4b51efb..ce591d090a 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -306,6 +306,7 @@ + From 1de9dbf18cd735eab646127e0f4c74e8918a5da6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 30 Mar 2015 20:13:03 +1100 Subject: [PATCH 35/38] Updates build process to include building a nuget Identity package to provide extensibility points in Umbraco. --- LICENSE.md | 9 ----- build/Build.bat | 1 + build/Build.proj | 24 ++++++++++++++ build/NuSpecs/UmbracoCms.Identity.nuspec | 24 ++++++++++++++ src/Umbraco.Web.UI/App_Start/Readme.txt | 33 +++++++++++++++++++ ...Startup.cs => UmbracoCustomOwinStartup.cs} | 2 +- ...artup.cs => UmbracoStandardOwinStartup.cs} | 2 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 +-- .../config/ClientDependency.config | 2 +- src/Umbraco.Web.UI/web.Template.Debug.config | 2 +- src/Umbraco.Web.UI/web.Template.config | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- ...tartup.cs => UmbracoDefaultOwinStartup.cs} | 4 +-- src/umbraco.sln | 1 + 14 files changed, 92 insertions(+), 20 deletions(-) delete mode 100644 LICENSE.md create mode 100644 build/NuSpecs/UmbracoCms.Identity.nuspec create mode 100644 src/Umbraco.Web.UI/App_Start/Readme.txt rename src/Umbraco.Web.UI/App_Start/{CustomUmbracoOwinStartup.cs => UmbracoCustomOwinStartup.cs} (97%) rename src/Umbraco.Web.UI/App_Start/{StandardUmbracoOwinStartup.cs => UmbracoStandardOwinStartup.cs} (96%) rename src/Umbraco.Web/{DefaultUmbracoOwinStartup.cs => UmbracoDefaultOwinStartup.cs} (90%) diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 149435b7cb..0000000000 --- a/LICENSE.md +++ /dev/null @@ -1,9 +0,0 @@ -# The MIT License (MIT) # - -Copyright (c) 2013 Umbraco - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/build/Build.bat b/build/Build.bat index 1b715dcc15..7a464b422c 100644 --- a/build/Build.bat +++ b/build/Build.bat @@ -56,6 +56,7 @@ REN .\_BuildOutput\WebApp\Xslt\Web.config Web.config.transform ECHO Packing the NuGet release files ..\src\.nuget\NuGet.exe Pack NuSpecs\UmbracoCms.Core.nuspec -Version %version% ..\src\.nuget\NuGet.exe Pack NuSpecs\UmbracoCms.nuspec -Version %version% +..\src\.nuget\NuGet.exe Pack NuSpecs\UmbracoCms.Identity.nuspec IF ERRORLEVEL 1 GOTO :showerror diff --git a/build/Build.proj b/build/Build.proj index 5be460d09b..ab346f7438 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -88,6 +88,7 @@ $(BuildFolderAbsolutePath)WebApp\ $(BuildFolderRelativeToProjects)WebPi\ $(BuildFolderAbsolutePath)WebPi\ + $(BuildFolderAbsolutePath)IdentityTemplates\ @@ -157,6 +158,28 @@ + + + + + + + + + + + + + + + + + + @@ -266,6 +289,7 @@ + $(BUILD_RELEASE) diff --git a/build/NuSpecs/UmbracoCms.Identity.nuspec b/build/NuSpecs/UmbracoCms.Identity.nuspec new file mode 100644 index 0000000000..c635b891f7 --- /dev/null +++ b/build/NuSpecs/UmbracoCms.Identity.nuspec @@ -0,0 +1,24 @@ + + + + UmbracoCms.Identity + 1.0.0 + Umbraco Extensibility for ASP.Net Identity + Umbraco HQ + Umbraco HQ + http://opensource.org/licenses/MIT + http://umbraco.com/ + http://umbraco.com/media/357769/100px_transparent.png + false + Installs files/classes to help with ASP.Net Identity extensibility for Umbraco back office + Installs classes to help with ASP.Net Identity extensibility for Umbraco + en-US + umbraco aspnet identity + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Start/Readme.txt b/src/Umbraco.Web.UI/App_Start/Readme.txt new file mode 100644 index 0000000000..b8897a27ea --- /dev/null +++ b/src/Umbraco.Web.UI/App_Start/Readme.txt @@ -0,0 +1,33 @@ + + _ _ __ __ ____ _____ _____ ____ + | | | | \/ | _ \| __ \ /\ / ____/ __ \ + | | | | \ / | |_) | |__) | / \ | | | | | | + | | | | |\/| | _ <| _ / / /\ \| | | | | | + | |__| | | | | |_) | | \ \ / ____ | |___| |__| | + \____/|_| |_|____/|_| \_/_/ \_\_____\____/ + +---------------------------------------------------- + +Umbraco extensibility code has been installed for ASP.Net Identity with Umbraco back office users + +The files have been installed into your App_Start folder if you have a Web Application project +or into App_Code if you have a Website project. + +All of these files include lots of code comments, documentation & notes to assist with extending +the ASP.Net Identity implementaion for back office users in Umbraco. For all 3rd party +ASP.Net providers, their dependencies will need to be manually installed. See comments in the +following files for full details: + +* StandardUmbracoOwinStartup.cs Includes code snippets to enable 3rd party ASP.Net Identity + providers to work with the Umbraco back office. + To enable the 'StandardUmbracoOwinStartup', update the web.config + appSetting "owin:appStartup" to be: "StandardUmbracoOwinStartup" + +* UmbracoCustomOwinStartup Includes code snippets to customize the Umbraco ASP.Net + Identity implementation for back office users as well as + snippets to enable 3rd party ASP.Net Identity providers to work. + To enable the 'UmbracoCustomOwinStartup', update the web.config + appSetting "owin:appStartup" to be: "UmbracoCustomOwinStartup" + +* UmbracoBackOfficeAuthExtensions Includes extension methods snippets to enable 3rd party ASP.Net + Identity providers to work with the Umbraco back office. \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Start/CustomUmbracoOwinStartup.cs b/src/Umbraco.Web.UI/App_Start/UmbracoCustomOwinStartup.cs similarity index 97% rename from src/Umbraco.Web.UI/App_Start/CustomUmbracoOwinStartup.cs rename to src/Umbraco.Web.UI/App_Start/UmbracoCustomOwinStartup.cs index 6769b760a9..a50e50fa1e 100644 --- a/src/Umbraco.Web.UI/App_Start/CustomUmbracoOwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Start/UmbracoCustomOwinStartup.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Security; using Umbraco.Web.Security.Identity; using Umbraco.Web.UI; -[assembly: OwinStartup("CustomUmbracoStartup", typeof(StandardUmbracoOwinStartup))] +[assembly: OwinStartup("CustomUmbracoOwinStartup", typeof(StandardUmbracoOwinStartup))] namespace Umbraco.Web.UI { diff --git a/src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs b/src/Umbraco.Web.UI/App_Start/UmbracoStandardOwinStartup.cs similarity index 96% rename from src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs rename to src/Umbraco.Web.UI/App_Start/UmbracoStandardOwinStartup.cs index a684efa2be..0c7a06d0f5 100644 --- a/src/Umbraco.Web.UI/App_Start/StandardUmbracoOwinStartup.cs +++ b/src/Umbraco.Web.UI/App_Start/UmbracoStandardOwinStartup.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Security; using Umbraco.Web.Security.Identity; using Umbraco.Web.UI; -[assembly: OwinStartup("StandardUmbracoStartup", typeof(StandardUmbracoOwinStartup))] +[assembly: OwinStartup("StandardUmbracoOwinStartup", typeof(StandardUmbracoOwinStartup))] namespace Umbraco.Web.UI { diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index ee56ec0fef..8d2c185043 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -357,9 +357,6 @@ Properties\SolutionInfo.cs - - - loadStarterKits.ascx ASPXCodeBehind @@ -2557,6 +2554,7 @@ + diff --git a/src/Umbraco.Web.UI/config/ClientDependency.config b/src/Umbraco.Web.UI/config/ClientDependency.config index 246bff7cac..7b53338f12 100644 --- a/src/Umbraco.Web.UI/config/ClientDependency.config +++ b/src/Umbraco.Web.UI/config/ClientDependency.config @@ -10,7 +10,7 @@ NOTES: * Compression/Combination/Minification is not enabled unless debug="false" is specified on the 'compiliation' element in the web.config * A new version will invalidate both client and server cache and create new persisted files --> - + - - - - - - - - - - diff --git a/build/NuSpecs/UmbracoCms.Identity.nuspec b/build/NuSpecs/UmbracoCms.Identity.nuspec deleted file mode 100644 index c635b891f7..0000000000 --- a/build/NuSpecs/UmbracoCms.Identity.nuspec +++ /dev/null @@ -1,24 +0,0 @@ - - - - UmbracoCms.Identity - 1.0.0 - Umbraco Extensibility for ASP.Net Identity - Umbraco HQ - Umbraco HQ - http://opensource.org/licenses/MIT - http://umbraco.com/ - http://umbraco.com/media/357769/100px_transparent.png - false - Installs files/classes to help with ASP.Net Identity extensibility for Umbraco back office - Installs classes to help with ASP.Net Identity extensibility for Umbraco - en-US - umbraco aspnet identity - - - - - - - - \ No newline at end of file diff --git a/build/NuSpecs/UmbracoExamine.PDF.nuspec b/build/NuSpecs/UmbracoExamine.PDF.nuspec deleted file mode 100644 index 5d1afff2b5..0000000000 --- a/build/NuSpecs/UmbracoExamine.PDF.nuspec +++ /dev/null @@ -1,23 +0,0 @@ - - - - UmbracoExamine.PDF - 0.7.0 - Umbraco HQ - Umbraco HQ - http://opensource.org/licenses/MIT - http://umbraco.com/ - http://umbraco.com/media/357769/100px_transparent.png - false - UmbracoExmine.PDF - umbraco - - - - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Start/Readme.txt b/src/Umbraco.Web.UI/App_Start/Readme.txt deleted file mode 100644 index b8897a27ea..0000000000 --- a/src/Umbraco.Web.UI/App_Start/Readme.txt +++ /dev/null @@ -1,33 +0,0 @@ - - _ _ __ __ ____ _____ _____ ____ - | | | | \/ | _ \| __ \ /\ / ____/ __ \ - | | | | \ / | |_) | |__) | / \ | | | | | | - | | | | |\/| | _ <| _ / / /\ \| | | | | | - | |__| | | | | |_) | | \ \ / ____ | |___| |__| | - \____/|_| |_|____/|_| \_/_/ \_\_____\____/ - ----------------------------------------------------- - -Umbraco extensibility code has been installed for ASP.Net Identity with Umbraco back office users - -The files have been installed into your App_Start folder if you have a Web Application project -or into App_Code if you have a Website project. - -All of these files include lots of code comments, documentation & notes to assist with extending -the ASP.Net Identity implementaion for back office users in Umbraco. For all 3rd party -ASP.Net providers, their dependencies will need to be manually installed. See comments in the -following files for full details: - -* StandardUmbracoOwinStartup.cs Includes code snippets to enable 3rd party ASP.Net Identity - providers to work with the Umbraco back office. - To enable the 'StandardUmbracoOwinStartup', update the web.config - appSetting "owin:appStartup" to be: "StandardUmbracoOwinStartup" - -* UmbracoCustomOwinStartup Includes code snippets to customize the Umbraco ASP.Net - Identity implementation for back office users as well as - snippets to enable 3rd party ASP.Net Identity providers to work. - To enable the 'UmbracoCustomOwinStartup', update the web.config - appSetting "owin:appStartup" to be: "UmbracoCustomOwinStartup" - -* UmbracoBackOfficeAuthExtensions Includes extension methods snippets to enable 3rd party ASP.Net - Identity providers to work with the Umbraco back office. \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs b/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs deleted file mode 100644 index 66ecfb1373..0000000000 --- a/src/Umbraco.Web.UI/App_Start/UmbracoBackOfficeAuthExtensions.cs +++ /dev/null @@ -1,308 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using System.Web; -using Microsoft.Owin; -using Owin; -using Umbraco.Core; -using Umbraco.Web.Security.Identity; -//using Microsoft.Owin.Security.Facebook; -//using Microsoft.Owin.Security.Google; -//using Microsoft.Owin.Security.OpenIdConnect; -//using Microsoft.Owin.Security.MicrosoftAccount; -//using Microsoft.IdentityModel.Clients.ActiveDirectory; - -namespace Umbraco.Web.UI -{ - public static class UmbracoBackOfficeAuthExtensions - { - /* - - /// - /// Configure microsoft account sign-in - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Nuget installation: - /// Microsoft.Owin.Security.MicrosoftAccount - /// - /// Microsoft account documentation for ASP.Net Identity can be found: - /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#MICROSOFT - /// http://blogs.msdn.com/b/webdev/archive/2012/09/19/configuring-your-asp-net-application-for-microsoft-oauth-account.aspx - /// - /// Microsoft apps can be created here: - /// - /// http://go.microsoft.com/fwlink/?LinkID=144070 - /// - /// - public static void ConfigureBackOfficeMicrosoftAuth(this IAppBuilder app, string clientId, string clientSecret, - string caption = "Microsoft", string style = "btn-microsoft", string icon = "fa-windows") - { - var msOptions = new MicrosoftAccountAuthenticationOptions - { - ClientId = clientId, - ClientSecret = clientSecret, - SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType - }; - msOptions.ForUmbracoBackOffice(style, icon); - msOptions.Caption = caption; - app.UseMicrosoftAccountAuthentication(msOptions); - } - - /// - /// Configure google sign-in - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Nuget installation: - /// Microsoft.Owin.Security.Google - /// - /// Google account documentation for ASP.Net Identity can be found: - /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#GOOGLE - /// - /// Google apps can be created here: - /// - /// https://developers.google.com/accounts/docs/OpenIDConnect#getcredentials - /// - /// - public static void ConfigureBackOfficeGoogleAuth(this IAppBuilder app, string clientId, string clientSecret, - string caption = "Google", string style = "btn-google-plus", string icon = "fa-google-plus") - { - var googleOptions = new GoogleOAuth2AuthenticationOptions - { - ClientId = clientId, - ClientSecret = clientSecret, - //In order to allow using different google providers on the front-end vs the back office, - // these settings are very important to make them distinguished from one another. - SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, - // By default this is '/signin-google', you will need to change that default value in your - // Google developer settings for your web-app in the "REDIRECT URIS" setting - CallbackPath = new PathString("/umbraco-google-signin") - }; - googleOptions.ForUmbracoBackOffice(style, icon); - googleOptions.Caption = caption; - app.UseGoogleAuthentication(googleOptions); - } - - /// - /// Configure facebook sign-in - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// Nuget installation: - /// Microsoft.Owin.Security.Facebook - /// - /// Facebook account documentation for ASP.Net Identity can be found: - /// - /// http://www.asp.net/web-api/overview/security/external-authentication-services#FACEBOOK - /// - /// Facebook apps can be created here: - /// - /// https://developers.facebook.com/ - /// - /// - public static void ConfigureBackOfficeFacebookAuth(this IAppBuilder app, string appId, string appSecret, - string caption = "Facebook", string style = "btn-facebook", string icon = "fa-facebook") - { - var fbOptions = new FacebookAuthenticationOptions - { - AppId = appId, - AppSecret = appSecret, - //In order to allow using different google providers on the front-end vs the back office, - // these settings are very important to make them distinguished from one another. - SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, - // By default this is '/signin-facebook', you will need to change that default value in your - // Facebook developer settings for your app in the Advanced settings under "Client OAuth Login" - // in the "Valid OAuth redirect URIs", specify the full URL, for example: http://mysite.com/umbraco-facebook-signin - CallbackPath = new PathString("/umbraco-facebook-signin") - }; - fbOptions.ForUmbracoBackOffice(style, icon); - fbOptions.Caption = caption; - app.UseFacebookAuthentication(fbOptions); - } - - /// - /// Configure ActiveDirectory sign-in - /// - /// - /// - /// - /// - /// The URL that will be redirected to after login is successful, example: http://mydomain.com/umbraco/; - /// - /// - /// - /// This by default is 'OpenIdConnect' but that doesn't match what ASP.Net Identity actually stores in the - /// loginProvider field in the database which looks something like this (for example): - /// https://sts.windows.net/3bb0b4c5-364f-4394-ad36-0f29f95e5ggg/ - /// and is based on your AD setup. This value needs to match in order for accounts to - /// detected as linked/un-linked in the back office. - /// - /// - /// - /// - /// - /// - /// Nuget installation: - /// Install-Package Microsoft.Owin.Security.OpenIdConnect - /// Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory - /// - /// ActiveDirectory account documentation for ASP.Net Identity can be found: - /// - /// https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet - /// - /// This configuration requires the NaiveSessionCache class below which will need to be un-commented - /// - /// - public static void ConfigureBackOfficeActiveDirectoryAuth(this IAppBuilder app, - string tenant, string clientId, string postLoginRedirectUri, string appKey, - string authType, - string caption = "Active Directory", string style = "btn-microsoft", string icon = "fa-windows") - { - const string aadInstance = "https://login.windows.net/{0}"; - const string graphResourceId = "https://graph.windows.net"; - - var authority = string.Format(CultureInfo.InvariantCulture, aadInstance, tenant); - var adOptions = new OpenIdConnectAuthenticationOptions - { - AuthenticationType = authType, - SignInAsAuthenticationType = Constants.Security.BackOfficeExternalAuthenticationType, - ClientId = clientId, - Authority = authority, - PostLogoutRedirectUri = postLoginRedirectUri, - Notifications = new OpenIdConnectAuthenticationNotifications() - { - // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away. - AuthorizationCodeReceived = (context) => - { - var credential = new ClientCredential(clientId, appKey); - var userObjectId = context.AuthenticationTicket.Identity.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value; - var authContext = new AuthenticationContext(authority, new NaiveSessionCache(userObjectId)); - var result = authContext.AcquireTokenByAuthorizationCode( - context.Code, - //NOTE: This URL needs to match EXACTLY the same path that is configured in the AD configuration. - new Uri( - HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Authority) + - HttpContext.Current.Request.RawUrl.EnsureStartsWith('/').EnsureEndsWith('/')), - credential, - graphResourceId); - - return Task.FromResult(0); - } - - } - - }; - adOptions.ForUmbracoBackOffice(style, icon); - adOptions.Caption = caption; - app.UseOpenIdConnectAuthentication(adOptions); - } - - */ - - } - - - - /* - - /// - /// A Session cache token storage which is required to initialize the AD Identity provider on startup - /// - /// - /// Based on the examples from the AD samples: - /// https://github.com/AzureADSamples/WebApp-WebAPI-OpenIDConnect-DotNet/blob/master/TodoListWebApp/Utils/NaiveSessionCache.cs - /// - /// There are some newer examples of different token storage including persistent storage here: - /// It would appear that this is better for whatever reason: https://github.com/OfficeDev/O365-WebApp-SingleTenant/blob/master/O365-WebApp-SingleTenant/Models/ADALTokenCache.cs - /// - /// The type of token storage will be dependent on your requirements but this should be fine for standard installations - /// - public class NaiveSessionCache : TokenCache - { - private static readonly object FileLock = new object(); - readonly string _cacheId; - public NaiveSessionCache(string userId) - { - _cacheId = userId + "_TokenCache"; - - AfterAccess = AfterAccessNotification; - BeforeAccess = BeforeAccessNotification; - Load(); - } - - public void Load() - { - lock (FileLock) - { - Deserialize((byte[])HttpContext.Current.Session[_cacheId]); - } - } - - public void Persist() - { - lock (FileLock) - { - // reflect changes in the persistent store - HttpContext.Current.Session[_cacheId] = Serialize(); - // once the write operation took place, restore the HasStateChanged bit to false - HasStateChanged = false; - } - } - - // Empties the persistent store. - public override void Clear() - { - base.Clear(); - HttpContext.Current.Session.Remove(_cacheId); - } - - public override void DeleteItem(TokenCacheItem item) - { - base.DeleteItem(item); - Persist(); - } - - // Triggered right before ADAL needs to access the cache. - // Reload the cache from the persistent store in case it changed since the last access. - void BeforeAccessNotification(TokenCacheNotificationArgs args) - { - Load(); - } - - // Triggered right after ADAL accessed the cache. - void AfterAccessNotification(TokenCacheNotificationArgs args) - { - // if the access operation resulted in a cache update - if (HasStateChanged) - { - Persist(); - } - } - } - - */ -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Start/UmbracoCustomOwinStartup.cs b/src/Umbraco.Web.UI/App_Start/UmbracoCustomOwinStartup.cs deleted file mode 100644 index a50e50fa1e..0000000000 --- a/src/Umbraco.Web.UI/App_Start/UmbracoCustomOwinStartup.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Microsoft.Owin; -using Owin; -using Umbraco.Core; -using Umbraco.Core.Security; -using Umbraco.Web.Security.Identity; -using Umbraco.Web.UI; - -[assembly: OwinStartup("CustomUmbracoOwinStartup", typeof(StandardUmbracoOwinStartup))] - -namespace Umbraco.Web.UI -{ - /// - /// A custom way to configure OWIN for Umbraco - /// - /// - /// The startup type is specified in appSettings under owin:appStartup - change it to "CustomUmbracoStartup" to use this class - /// - /// This startup class would allow you to customize the Identity IUserStore and/or IUserManager for the Umbraco Backoffice - /// - public class CustomUmbracoOwinStartup - { - public void Configuration(IAppBuilder app) - { - //Configure the Identity user manager for use with Umbraco Back office - // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) - app.ConfigureUserManagerForUmbracoBackOffice( - ApplicationContext.Current, - Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider()); - - //Ensure owin is configured for Umbraco back office authentication - app - .UseUmbracoBackOfficeCookieAuthentication() - .UseUmbracoBackOfficeExternalCookieAuthentication(); - - /* - * Configure external logins for the back office: - * - * Depending on the authentication sources you would like to enable, you will need to install - * certain Nuget packages. - * - * For Google auth: Install-Package Microsoft.Owin.Security.Google - * For Facebook auth: Install-Package Microsoft.Owin.Security.Facebook - * For Microsoft auth: Install-Package Microsoft.Owin.Security.MicrosoftAccount - * - * There are many more providers such as Twitter, Yahoo, ActiveDirectory, etc... most information can - * be found here: http://www.asp.net/web-api/overview/security/external-authentication-services - * - * The source for these methods is located in ~/App_Code/IdentityAuthExtensions.cs, you will need to un-comment - * the methods that you would like to use. Each method contains documentation and links to - * documentation for reference. You can also tweak the code in those extension - * methods to suit your needs. - */ - - //app.ConfigureBackOfficeGoogleAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); - //app.ConfigureBackOfficeFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); - //app.ConfigureBackOfficeMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); - //app.ConfigureBackOfficeActiveDirectoryAuth("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/App_Start/UmbracoStandardOwinStartup.cs b/src/Umbraco.Web.UI/App_Start/UmbracoStandardOwinStartup.cs deleted file mode 100644 index 0c7a06d0f5..0000000000 --- a/src/Umbraco.Web.UI/App_Start/UmbracoStandardOwinStartup.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.Owin; -using Owin; -using Umbraco.Core; -using Umbraco.Core.Security; -using Umbraco.Web.Security.Identity; -using Umbraco.Web.UI; - -[assembly: OwinStartup("StandardUmbracoOwinStartup", typeof(StandardUmbracoOwinStartup))] - -namespace Umbraco.Web.UI -{ - /// - /// The standard way to configure OWIN for Umbraco - /// - /// - /// The startup type is specified in appSettings under owin:appStartup - change it to "StandardUmbracoStartup" to use this class - /// - public class StandardUmbracoOwinStartup : DefaultUmbracoOwinStartup - { - public override void Configuration(IAppBuilder app) - { - //ensure the default options are configured - base.Configuration(app); - - /* - * Configure external logins for the back office: - * - * Depending on the authentication sources you would like to enable, you will need to install - * certain Nuget packages. - * - * For Google auth: Install-Package Microsoft.Owin.Security.Google - * For Facebook auth: Install-Package Microsoft.Owin.Security.Facebook - * For Microsoft auth: Install-Package Microsoft.Owin.Security.MicrosoftAccount - * - * There are many more providers such as Twitter, Yahoo, ActiveDirectory, etc... most information can - * be found here: http://www.asp.net/web-api/overview/security/external-authentication-services - * - * The source for these methods is located in ~/App_Code/IdentityAuthExtensions.cs, you will need to un-comment - * the methods that you would like to use. Each method contains documentation and links to - * documentation for reference. You can also tweak the code in those extension - * methods to suit your needs. - */ - - //app.ConfigureBackOfficeGoogleAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); - //app.ConfigureBackOfficeFacebookAuth("YOUR_APP_ID", "YOUR_APP_SECRET"); - //app.ConfigureBackOfficeMicrosoftAuth("YOUR_CLIENT_ID", "YOUR_CLIENT_SECRET"); - //app.ConfigureBackOfficeActiveDirectoryAuth("YOUR_TENANT", "YOUR_CLIENT_ID", "YOUR_POST_LOGIN_REDIRECT_URL", "YOUR_APP_KEY", "YOUR_AUTH_TYPE"); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 8d2c185043..095cf28afc 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2554,7 +2554,6 @@ - diff --git a/src/umbraco.sln b/src/umbraco.sln index 71c8e27411..d839dd0a01 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -31,7 +31,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C ..\build\NuSpecs\UmbracoCms.Core.AllBinaries.nuspec = ..\build\NuSpecs\UmbracoCms.Core.AllBinaries.nuspec ..\build\NuSpecs\UmbracoCms.Core.nuspec = ..\build\NuSpecs\UmbracoCms.Core.nuspec ..\build\NuSpecs\UmbracoCms.Core.Symbols.nuspec = ..\build\NuSpecs\UmbracoCms.Core.Symbols.nuspec - ..\build\NuSpecs\UmbracoCms.Identity.nuspec = ..\build\NuSpecs\UmbracoCms.Identity.nuspec ..\build\NuSpecs\UmbracoCms.nuspec = ..\build\NuSpecs\UmbracoCms.nuspec EndProjectSection EndProject From f01990061988089274a06eb49a3aeb353ab09c5e Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Apr 2015 13:55:33 +1100 Subject: [PATCH 38/38] oops, not sure how the LICENSE got deleted, removed AD identity nuspec since that is in a diff repo now. Removes commented out code in umb module for auth - this is done by cookie middleware now. --- LICENSE.md | 9 +++++ .../NuSpecs/UmbracoCms.ActiveDirectory.nuspec | 26 ------------- src/Umbraco.Web/UmbracoModule.cs | 39 ------------------- src/umbraco.sln | 1 - 4 files changed, 9 insertions(+), 66 deletions(-) create mode 100644 LICENSE.md delete mode 100644 build/NuSpecs/UmbracoCms.ActiveDirectory.nuspec diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000..c5560c3ce1 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +# The MIT License (MIT) # + +Copyright (c) 2013 Umbraco + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/build/NuSpecs/UmbracoCms.ActiveDirectory.nuspec b/build/NuSpecs/UmbracoCms.ActiveDirectory.nuspec deleted file mode 100644 index e25bd9825d..0000000000 --- a/build/NuSpecs/UmbracoCms.ActiveDirectory.nuspec +++ /dev/null @@ -1,26 +0,0 @@ - - - - UmbracoCms.Identity.ActiveDirectory - 1.0.0 - Umbraco Extensibility for ASP.Net Identity Active Directory - Umbraco HQ - Umbraco HQ - http://opensource.org/licenses/MIT - http://umbraco.com/ - http://umbraco.com/media/357769/100px_transparent.png - false - Installs files/classes to help with ASP.Net Identity Active Directory provider extensibility for Umbraco back office - Installs classes to help with ASP.Net Identity Active Directory extensibility for Umbraco - en-US - umbraco aspnet identity activedirectory - - - - - - - - - - \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 719f7135c4..772573d164 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -166,43 +166,6 @@ namespace Umbraco.Web RewriteToUmbracoHandler(httpContext, pcr); } - /// - /// Authenticates the request by reading the FormsAuthentication cookie and setting the - /// context and thread principle object - /// - /// - /// - /// - /// We will set the identity, culture, etc... for any request that is: - /// * A back office request - /// * An installer request - /// * A /base request (since these can be back office web service requests) - /// - static void AuthenticateRequest(object sender, EventArgs e) - { - //var app = (HttpApplication)sender; - //var http = new HttpContextWrapper(app.Context); - - //// do not process if client-side request - //if (http.Request.Url.IsClientSideRequest()) - // return; - - //var req = new HttpRequestWrapper(app.Request); - - //if (ShouldAuthenticateRequest(req, UmbracoContext.Current.OriginalRequestUrl)) - //{ - // //TODO: Here we should have an authentication mechanism, this mechanism should be smart in the way that the ASP.Net 5 pipeline works - // // in which each registered handler will attempt to authenticate and if it fails it will just call Next() so the next handler - // // executes. If it is successful, it doesn't call next and assigns the current user/principal. - // // This might actually all be possible with ASP.Net Identity and how it is setup to work already, need to investigate. - - // var ticket = http.GetUmbracoAuthTicket(); - - // http.AuthenticateCurrentRequest(ticket, ShouldIgnoreTicketRenew(UmbracoContext.Current.OriginalRequestUrl, http) == false); - //} - - } - #endregion #region Methods @@ -590,8 +553,6 @@ namespace Umbraco.Web BeginRequest(new HttpContextWrapper(httpContext)); }; - app.AuthenticateRequest += AuthenticateRequest; - app.PostResolveRequestCache += (sender, e) => { var httpContext = ((HttpApplication)sender).Context; diff --git a/src/umbraco.sln b/src/umbraco.sln index d839dd0a01..7c550c17ba 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -27,7 +27,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B5BD12C1 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuSpecs", "NuSpecs", "{227C3B55-80E5-4E7E-A802-BE16C5128B9D}" ProjectSection(SolutionItems) = preProject - ..\build\NuSpecs\UmbracoCms.ActiveDirectory.nuspec = ..\build\NuSpecs\UmbracoCms.ActiveDirectory.nuspec ..\build\NuSpecs\UmbracoCms.Core.AllBinaries.nuspec = ..\build\NuSpecs\UmbracoCms.Core.AllBinaries.nuspec ..\build\NuSpecs\UmbracoCms.Core.nuspec = ..\build\NuSpecs\UmbracoCms.Core.nuspec ..\build\NuSpecs\UmbracoCms.Core.Symbols.nuspec = ..\build\NuSpecs\UmbracoCms.Core.Symbols.nuspec