From f2e319a01fb9246415e303f509b7bd1b3bd6e0ed Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 10 Apr 2015 14:22:09 +1000 Subject: [PATCH] Updates the UmbracoBackOfficeIdentity to have better support for claims and adds unit tests for it. Creates OwinLogger's and methods to apply them. Updates security methods to ensure that a UmbracoBackOfficeIdentity is returned even from a normal ClaimsIdentity which will be the case with bearer tokens. Updates the angular anti-forgery checker to be ignore if the auth type is not cookie based. Adds a simple token server provider that people can use if they want. Now token authentication is working. --- src/Umbraco.Core/Constants-Web.cs | 4 +- src/Umbraco.Core/Logging/OwinLogger.cs | 63 ++++++ src/Umbraco.Core/Logging/OwinLoggerFactory.cs | 24 +++ .../Security/AuthenticationExtensions.cs | 12 +- .../BackOfficeClaimsIdentityFactory.cs | 27 +-- .../Security/UmbracoBackOfficeIdentity.cs | 174 +++++++++++++--- src/Umbraco.Core/Umbraco.Core.csproj | 2 + .../UmbracoBackOfficeIdentityTests.cs | 189 ++++++++++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + .../Editors/BackOfficeController.cs | 2 +- .../Security/Identity/AppBuilderExtensions.cs | 12 +- .../BackOfficeAuthorizationServerProvider.cs | 34 ++++ src/Umbraco.Web/Umbraco.Web.csproj | 1 + src/Umbraco.Web/UmbracoDefaultOwinStartup.cs | 4 + ...alidateAngularAntiForgeryTokenAttribute.cs | 16 +- 15 files changed, 519 insertions(+), 46 deletions(-) create mode 100644 src/Umbraco.Core/Logging/OwinLogger.cs create mode 100644 src/Umbraco.Core/Logging/OwinLoggerFactory.cs create mode 100644 src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs create mode 100644 src/Umbraco.Web/Security/Identity/BackOfficeAuthorizationServerProvider.cs diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index a936b2e388..ac2223f98c 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -37,9 +37,7 @@ 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 AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp"; public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid"; } diff --git a/src/Umbraco.Core/Logging/OwinLogger.cs b/src/Umbraco.Core/Logging/OwinLogger.cs new file mode 100644 index 0000000000..f01c83b738 --- /dev/null +++ b/src/Umbraco.Core/Logging/OwinLogger.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics; + +namespace Umbraco.Core.Logging +{ + internal class OwinLogger : Microsoft.Owin.Logging.ILogger + { + private readonly ILogger _logger; + private readonly Lazy _type; + + public OwinLogger(ILogger logger, Lazy type) + { + _logger = logger; + _type = type; + } + + /// + /// Aggregates most logging patterns to a single method. This must be compatible with the Func representation in the OWIN environment. + /// To check IsEnabled call WriteCore with only TraceEventType and check the return value, no event will be written. + /// + /// + /// + public bool WriteCore(TraceEventType eventType, int eventId, object state, Exception exception, Func formatter) + { + if (state == null) state = ""; + switch (eventType) + { + case TraceEventType.Critical: + _logger.Error(_type.Value, string.Format("Event Id: {0}, state: {1}", eventId, state), exception ?? new Exception("Critical error")); + return true; + case TraceEventType.Error: + _logger.Error(_type.Value, string.Format("Event Id: {0}, state: {1}", eventId, state), exception ?? new Exception("Error")); + return true; + case TraceEventType.Warning: + _logger.Warn(_type.Value, string.Format("Event Id: {0}, state: {1}", eventId, state)); + return true; + case TraceEventType.Information: + _logger.Info(_type.Value, string.Format("Event Id: {0}, state: {1}", eventId, state)); + return true; + case TraceEventType.Verbose: + _logger.Debug(_type.Value, string.Format("Event Id: {0}, state: {1}", eventId, state)); + return true; + case TraceEventType.Start: + _logger.Debug(_type.Value, string.Format("Event Id: {0}, state: {1}", eventId, state)); + return true; + case TraceEventType.Stop: + _logger.Debug(_type.Value, string.Format("Event Id: {0}, state: {1}", eventId, state)); + return true; + case TraceEventType.Suspend: + _logger.Debug(_type.Value, string.Format("Event Id: {0}, state: {1}", eventId, state)); + return true; + case TraceEventType.Resume: + _logger.Debug(_type.Value, string.Format("Event Id: {0}, state: {1}", eventId, state)); + return true; + case TraceEventType.Transfer: + _logger.Debug(_type.Value, string.Format("Event Id: {0}, state: {1}", eventId, state)); + return true; + default: + throw new ArgumentOutOfRangeException("eventType"); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Logging/OwinLoggerFactory.cs b/src/Umbraco.Core/Logging/OwinLoggerFactory.cs new file mode 100644 index 0000000000..c60ecd1f04 --- /dev/null +++ b/src/Umbraco.Core/Logging/OwinLoggerFactory.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Owin.Logging; + +namespace Umbraco.Core.Logging +{ + internal class OwinLoggerFactory : ILoggerFactory + { + /// + /// Creates a new ILogger instance of the given name. + /// + /// + /// + public Microsoft.Owin.Logging.ILogger Create(string name) + { + return new OwinLogger( + LoggerResolver.HasCurrent ? LoggerResolver.Current.Logger : new DebugDiagnosticsLogger(), + new Lazy(() => Type.GetType(name) ?? typeof (OwinLogger))); + } + } +} diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 9addb2e782..802bd555d3 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -5,6 +5,7 @@ using System.Collections.Specialized; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; +using System.Security.Claims; using System.Security.Principal; using System.Threading; using System.Web; @@ -14,6 +15,7 @@ using Microsoft.Owin; using Newtonsoft.Json; using Umbraco.Core.Configuration; using Umbraco.Core.Models.Membership; +using Microsoft.Owin; namespace Umbraco.Core.Security { @@ -95,8 +97,14 @@ namespace Umbraco.Core.Security { if (http == null) throw new ArgumentNullException("http"); if (http.User == null) return null; //there's no user at all so no identity - var identity = http.User.Identity as UmbracoBackOfficeIdentity; - if (identity != null) return identity; + + //If it's already a UmbracoBackOfficeIdentity + var backOfficeIdentity = http.User.Identity as UmbracoBackOfficeIdentity; + if (backOfficeIdentity != null) return backOfficeIdentity; + + //Otherwise convert to a UmbracoBackOfficeIdentity + var claimsIdentity = http.User.Identity as ClaimsIdentity; + if (claimsIdentity != null) return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); if (authenticateRequestIfNotFound == false) return null; diff --git a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs index b6d19b78eb..8529132bb5 100644 --- a/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs +++ b/src/Umbraco.Core/Security/BackOfficeClaimsIdentityFactory.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNet.Identity; @@ -17,17 +18,19 @@ namespace Umbraco.Core.Security { 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.AllowedSections, - Culture = user.Culture, - Roles = user.Roles.Select(x => x.RoleId).ToArray(), - StartContentNode = user.StartContentId, - StartMediaNode = user.StartMediaId - }); + var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity, + //set a new session id + new UserData(Guid.NewGuid().ToString("N")) + { + Id = user.Id, + Username = user.UserName, + RealName = user.Name, + AllowedApplications = user.AllowedSections, + Culture = user.Culture, + Roles = user.Roles.Select(x => x.RoleId).ToArray(), + StartContentNode = user.StartContentId, + StartMediaNode = user.StartMediaId + }); return umbracoIdentity; } diff --git a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs index bf3bc7016c..c2f56522fb 100644 --- a/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs +++ b/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs @@ -1,10 +1,14 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Runtime.Serialization; using System.Security.Claims; using System.Security.Principal; using System.Web; using System.Web.Security; +using Microsoft.AspNet.Identity; using Newtonsoft.Json; +using Umbraco.Core.Configuration; namespace Umbraco.Core.Security { @@ -19,6 +23,57 @@ namespace Umbraco.Core.Security [Serializable] public class UmbracoBackOfficeIdentity : FormsIdentity { + public static UmbracoBackOfficeIdentity FromClaimsIdentity(ClaimsIdentity identity) + { + foreach (var t in RequiredBackOfficeIdentityClaimTypes) + { + //if the identity doesn't have the claim, or the claim value is null + if (identity.HasClaim(x => x.Type == t) == false || identity.HasClaim(x => x.Type == t && x.Value.IsNullOrWhiteSpace())) + { + throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the required claim " + t + " is missing"); + } + } + + var username = identity.GetUserName(); + var session = identity.FindFirstValue(Constants.Security.SessionIdClaimType); + var startContentId = identity.FindFirstValue(Constants.Security.StartContentNodeIdClaimType); + var startMediaId = identity.FindFirstValue(Constants.Security.StartMediaNodeIdClaimType); + + var culture = identity.FindFirstValue(ClaimTypes.Locality); + var id = identity.FindFirstValue(ClaimTypes.NameIdentifier); + var realName = identity.FindFirstValue(ClaimTypes.GivenName); + + if (username == null || startContentId == null || startMediaId == null + || culture == null || id == null + || realName == null || session == null) + throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since there are missing required claims"); + + int startContentIdAsInt; + int startMediaIdAsInt; + if (int.TryParse(startContentId, out startContentIdAsInt) == false || int.TryParse(startMediaId, out startMediaIdAsInt) == false) + { + throw new InvalidOperationException("Cannot create a " + typeof(UmbracoBackOfficeIdentity) + " from " + typeof(ClaimsIdentity) + " since the data is not formatted correctly"); + } + + var roles = identity.FindAll(x => x.Type == DefaultRoleClaimType).Select(role => role.Value).ToList(); + var allowedApps = identity.FindAll(x => x.Type == Constants.Security.AllowedApplicationsClaimType).Select(app => app.Value).ToList(); + + var userData = new UserData(session) + { + SessionId = session, + AllowedApplications = allowedApps.ToArray(), + Culture = culture, + Id = id, + Roles = roles.ToArray(), + Username = username, + RealName = realName, + StartContentNode = startContentIdAsInt, + StartMediaNode = startMediaIdAsInt + }; + + return new UmbracoBackOfficeIdentity(identity, userData); + } + /// /// Create a back office identity based on user data /// @@ -44,9 +99,15 @@ namespace Umbraco.Core.Security if (claimsIdentity == null) throw new ArgumentNullException("claimsIdentity"); if (userdata == null) throw new ArgumentNullException("userdata"); + if (claimsIdentity is FormsIdentity) + { + //since it's a forms auth ticket, it is from a cookie so add that claim + AddClaim(new Claim(ClaimTypes.CookiePath, "/", ClaimValueTypes.String, Issuer, Issuer, this)); + } + _currentIssuer = claimsIdentity.AuthenticationType; UserData = userdata; - AddClaims(claimsIdentity); + AddExistingClaims(claimsIdentity); Actor = claimsIdentity; AddUserDataClaims(); } @@ -58,6 +119,9 @@ namespace Umbraco.Core.Security public UmbracoBackOfficeIdentity(FormsAuthenticationTicket ticket) : base(ticket) { + //since it's a forms auth ticket, it is from a cookie so add that claim + AddClaim(new Claim(ClaimTypes.CookiePath, "/", ClaimValueTypes.String, Issuer, Issuer, this)); + UserData = JsonConvert.DeserializeObject(ticket.UserData); AddUserDataClaims(); } @@ -72,7 +136,7 @@ namespace Umbraco.Core.Security if (identity.Actor != null) { _currentIssuer = identity.AuthenticationType; - AddClaims(identity); + AddExistingClaims(identity); Actor = identity.Clone(); } @@ -83,43 +147,98 @@ namespace Umbraco.Core.Security public const string Issuer = "UmbracoBackOffice"; private readonly string _currentIssuer = Issuer; - private void AddClaims(ClaimsIdentity claimsIdentity) + /// + /// Used during ctor to add existing claims from an existing ClaimsIdentity + /// + /// + private void AddExistingClaims(ClaimsIdentity claimsIdentity) { foreach (var claim in claimsIdentity.Claims) { + //In one special case we will replace a claim if it exists already and that is the + // Forms auth claim for name which automatically gets added + TryRemoveClaim(FindFirst(x => x.Type == claim.Type && x.Issuer == "Forms")); + AddClaim(claim); } } + /// + /// Returns the required claim types for a back office identity + /// + /// + /// This does not incude the role claim type or allowed apps type since that is a collection and in theory could be empty + /// + public static IEnumerable RequiredBackOfficeIdentityClaimTypes + { + get + { + return new[] + { + ClaimTypes.NameIdentifier, //id + ClaimTypes.Name, //username + ClaimTypes.GivenName, + Constants.Security.StartContentNodeIdClaimType, + Constants.Security.StartMediaNodeIdClaimType, + ClaimTypes.Locality, + Constants.Security.SessionIdClaimType + }; + } + } + /// /// Adds claims based on the UserData data /// private void AddUserDataClaims() { - AddClaims(new[] + //This is the id that 'identity' uses to check for the user id + if (HasClaim(x => x.Type == ClaimTypes.NameIdentifier) == false) + AddClaim(new Claim(ClaimTypes.NameIdentifier, UserData.Id.ToString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); + + if (HasClaim(x => x.Type == ClaimTypes.Name) == false) + AddClaim(new Claim(ClaimTypes.Name, UserData.Username, ClaimValueTypes.String, Issuer, Issuer, this)); + + if (HasClaim(x => x.Type == ClaimTypes.GivenName) == false) + AddClaim(new Claim(ClaimTypes.GivenName, UserData.RealName, ClaimValueTypes.String, Issuer, Issuer, this)); + + if (HasClaim(x => x.Type == Constants.Security.StartContentNodeIdClaimType) == false) + AddClaim(new Claim(Constants.Security.StartContentNodeIdClaimType, StartContentNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); + + if (HasClaim(x => x.Type == Constants.Security.StartMediaNodeIdClaimType) == false) + AddClaim(new Claim(Constants.Security.StartMediaNodeIdClaimType, StartMediaNode.ToInvariantString(), ClaimValueTypes.Integer32, Issuer, Issuer, this)); + + if (HasClaim(x => x.Type == ClaimTypes.Locality) == false) + AddClaim(new Claim(ClaimTypes.Locality, Culture, ClaimValueTypes.String, Issuer, Issuer, this)); + + if (HasClaim(x => x.Type == Constants.Security.SessionIdClaimType) == false) + AddClaim(new Claim(Constants.Security.SessionIdClaimType, SessionId, ClaimValueTypes.String, Issuer, Issuer, this)); + + //Add each app as a separate claim + if (HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false) { - //This is the id that 'identity' uses to check for the user id - new Claim(ClaimTypes.NameIdentifier, Id.ToString(), null, Issuer, Issuer, this), - - 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.CultureClaimType, Culture, 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) - }); - - //TODO: Find out why sessionid is null - this depends on how the identity is created! - if (SessionId.IsNullOrWhiteSpace() == false) - { - AddClaim(new Claim(Constants.Security.SessionIdClaimType, SessionId, null, Issuer, Issuer, this)); + foreach (var application in AllowedApplications) + { + AddClaim(new Claim(Constants.Security.AllowedApplicationsClaimType, application, ClaimValueTypes.String, Issuer, Issuer, this)); + } } + + //Claims are added by the ClaimsIdentityFactory because our UserStore supports roles, however this identity might + // not be made with that factory if it was created with a FormsAuthentication ticket so perform the check + if (HasClaim(x => x.Type == DefaultRoleClaimType) == false) + { + //manually add them based on the UserData + foreach (var roleName in UserData.Roles) + { + AddClaim(new Claim(RoleClaimType, roleName, ClaimValueTypes.String, Issuer, Issuer, this)); + } + } + + ////TODO: Find out why sessionid is null - this depends on how the identity is created! + //// in this case generate one? + //if (SessionId.IsNullOrWhiteSpace() == false) + //{ + + //} } @@ -161,6 +280,11 @@ namespace Umbraco.Core.Security get { return UserData.RealName; } } + public string Username + { + get { return UserData.Username; } + } + public string Culture { get { return UserData.Culture; } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 69ef3fec88..e7416d428e 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -345,6 +345,8 @@ + + diff --git a/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs new file mode 100644 index 0000000000..4119ab4f86 --- /dev/null +++ b/src/Umbraco.Tests/Security/UmbracoBackOfficeIdentityTests.cs @@ -0,0 +1,189 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.Web.Security; +using Newtonsoft.Json; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Security; + +namespace Umbraco.Tests.Security +{ + [TestFixture] + public class UmbracoBackOfficeIdentityTests + { + + public const string TestIssuer = "TestIssuer"; + + [Test] + public void Create_From_Claims_Identity() + { + var sessionId = Guid.NewGuid().ToString(); + var claimsIdentity = new ClaimsIdentity(new[] + { + //This is the id that 'identity' uses to check for the user id + new Claim(ClaimTypes.NameIdentifier, "1234", ClaimValueTypes.Integer32, TestIssuer, TestIssuer), + //This is the id that 'identity' uses to check for the username + new Claim(ClaimTypes.Name, "testing", ClaimValueTypes.String, TestIssuer, TestIssuer), + new Claim(ClaimTypes.GivenName, "hello world", ClaimValueTypes.String, TestIssuer, TestIssuer), + new Claim(Constants.Security.StartContentNodeIdClaimType, "-1", ClaimValueTypes.Integer32, TestIssuer, TestIssuer), + new Claim(Constants.Security.StartMediaNodeIdClaimType, "5543", ClaimValueTypes.Integer32, TestIssuer, TestIssuer), + new Claim(Constants.Security.AllowedApplicationsClaimType, "content", ClaimValueTypes.String, TestIssuer, TestIssuer), + new Claim(Constants.Security.AllowedApplicationsClaimType, "media", ClaimValueTypes.String, TestIssuer, TestIssuer), + new Claim(ClaimTypes.Locality, "en-us", ClaimValueTypes.String, TestIssuer, TestIssuer), + new Claim(Constants.Security.SessionIdClaimType, sessionId, Constants.Security.SessionIdClaimType, TestIssuer, TestIssuer), + new Claim(ClaimsIdentity.DefaultRoleClaimType, "admin", ClaimValueTypes.String, TestIssuer, TestIssuer), + }); + + var backofficeIdentity = UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); + + Assert.AreEqual("1234", backofficeIdentity.Id); + Assert.AreEqual(sessionId, backofficeIdentity.SessionId); + Assert.AreEqual("testing", backofficeIdentity.Username); + Assert.AreEqual("hello world", backofficeIdentity.RealName); + Assert.AreEqual(-1, backofficeIdentity.StartContentNode); + Assert.AreEqual(5543, backofficeIdentity.StartMediaNode); + Assert.IsTrue(new[] {"content", "media"}.SequenceEqual(backofficeIdentity.AllowedApplications)); + Assert.AreEqual("en-us", backofficeIdentity.Culture); + Assert.IsTrue(new[] { "admin" }.SequenceEqual(backofficeIdentity.Roles)); + + Assert.AreEqual(10, backofficeIdentity.Claims.Count()); + } + + [Test] + public void Create_From_Claims_Identity_Missing_Required_Claim() + { + var claimsIdentity = new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.NameIdentifier, "1234", ClaimValueTypes.Integer32, TestIssuer, TestIssuer), + new Claim(ClaimTypes.Name, "testing", ClaimValueTypes.String, TestIssuer, TestIssuer), + }); + + Assert.Throws(() => UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity)); + } + + [Test] + public void Create_From_Claims_Identity_Required_Claim_Null() + { + var sessionId = Guid.NewGuid().ToString(); + var claimsIdentity = new ClaimsIdentity(new[] + { + //null or empty + new Claim(ClaimTypes.NameIdentifier, "", ClaimValueTypes.Integer32, TestIssuer, TestIssuer), + new Claim(ClaimTypes.Name, "testing", ClaimValueTypes.String, TestIssuer, TestIssuer), + new Claim(ClaimTypes.GivenName, "hello world", ClaimValueTypes.String, TestIssuer, TestIssuer), + new Claim(Constants.Security.StartContentNodeIdClaimType, "-1", ClaimValueTypes.Integer32, TestIssuer, TestIssuer), + new Claim(Constants.Security.StartMediaNodeIdClaimType, "5543", ClaimValueTypes.Integer32, TestIssuer, TestIssuer), + new Claim(Constants.Security.AllowedApplicationsClaimType, "content", ClaimValueTypes.String, TestIssuer, TestIssuer), + new Claim(Constants.Security.AllowedApplicationsClaimType, "media", ClaimValueTypes.String, TestIssuer, TestIssuer), + new Claim(ClaimTypes.Locality, "en-us", ClaimValueTypes.String, TestIssuer, TestIssuer), + new Claim(Constants.Security.SessionIdClaimType, sessionId, Constants.Security.SessionIdClaimType, TestIssuer, TestIssuer), + new Claim(ClaimsIdentity.DefaultRoleClaimType, "admin", ClaimValueTypes.String, TestIssuer, TestIssuer), + }); + + Assert.Throws(() => UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity)); + } + + [Test] + public void Create_With_User_Data() + { + var sessionId = Guid.NewGuid().ToString(); + var userData = new UserData(sessionId) + { + AllowedApplications = new[] {"content", "media"}, + Culture = "en-us", + Id = 1234, + RealName = "hello world", + Roles = new[] {"admin"}, + StartContentNode = -1, + StartMediaNode = 654, + Username = "testing" + }; + + var identity = new UmbracoBackOfficeIdentity(userData); + + Assert.AreEqual(10, identity.Claims.Count()); + } + + [Test] + public void Create_With_Claims_And_User_Data() + { + var sessionId = Guid.NewGuid().ToString(); + var userData = new UserData(sessionId) + { + AllowedApplications = new[] { "content", "media" }, + Culture = "en-us", + Id = 1234, + RealName = "hello world", + Roles = new[] { "admin" }, + StartContentNode = -1, + StartMediaNode = 654, + Username = "testing" + }; + + var claimsIdentity = new ClaimsIdentity(new[] + { + new Claim("TestClaim1", "test", ClaimValueTypes.Integer32, TestIssuer, TestIssuer), + new Claim("TestClaim1", "test", ClaimValueTypes.Integer32, TestIssuer, TestIssuer) + }); + + var backofficeIdentity = new UmbracoBackOfficeIdentity(claimsIdentity, userData); + + Assert.AreEqual(12, backofficeIdentity.Claims.Count()); + } + + [Test] + public void Create_With_Forms_Ticket() + { + var sessionId = Guid.NewGuid().ToString(); + var userData = new UserData(sessionId) + { + AllowedApplications = new[] { "content", "media" }, + Culture = "en-us", + Id = 1234, + RealName = "hello world", + Roles = new[] { "admin" }, + StartContentNode = -1, + StartMediaNode = 654, + Username = "testing" + }; + + var ticket = new FormsAuthenticationTicket(1, userData.Username, DateTime.Now, DateTime.Now.AddDays(1), true, + JsonConvert.SerializeObject(userData)); + + var identity = new UmbracoBackOfficeIdentity(ticket); + + Assert.AreEqual(11, identity.Claims.Count()); + } + + [Test] + public void Clone() + { + var sessionId = Guid.NewGuid().ToString(); + var userData = new UserData(sessionId) + { + AllowedApplications = new[] { "content", "media" }, + Culture = "en-us", + Id = 1234, + RealName = "hello world", + Roles = new[] { "admin" }, + StartContentNode = -1, + StartMediaNode = 654, + Username = "testing" + }; + + var ticket = new FormsAuthenticationTicket(1, userData.Username, DateTime.Now, DateTime.Now.AddDays(1), true, + JsonConvert.SerializeObject(userData)); + + var identity = new UmbracoBackOfficeIdentity(ticket); + + var cloned = identity.Clone(); + + Assert.AreEqual(11, cloned.Claims.Count()); + } + + } +} diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 38bfe1ddfa..22dc487583 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -181,6 +181,7 @@ + diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 0313dd868f..dc9a8f9bed 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -93,7 +93,7 @@ namespace Umbraco.Web.Editors { var cultureInfo = culture == null //if the user is logged in, get their culture, otherwise default to 'en' - ? User.Identity.IsAuthenticated && User.Identity is UmbracoBackOfficeIdentity + ? User.Identity.IsAuthenticated ? Security.CurrentUser.GetUserCulture(Services.TextService) : CultureInfo.GetCultureInfo("en") : CultureInfo.GetCultureInfo(culture); diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index 81ae20965c..c7b12f243e 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using Microsoft.Owin.Extensions; +using Microsoft.Owin.Logging; using Microsoft.Owin.Security; using Microsoft.Owin.Security.Cookies; using Owin; @@ -20,6 +21,15 @@ namespace Umbraco.Web.Security.Identity { public static class AppBuilderExtensions { + /// + /// Sets the OWIN logger to use Umbraco's logging system + /// + /// + public static void SetUmbracoLoggerFactory(this IAppBuilder app) + { + app.SetLoggerFactory(new OwinLoggerFactory()); + } + #region Backoffice /// @@ -112,7 +122,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. diff --git a/src/Umbraco.Web/Security/Identity/BackOfficeAuthorizationServerProvider.cs b/src/Umbraco.Web/Security/Identity/BackOfficeAuthorizationServerProvider.cs new file mode 100644 index 0000000000..0f049fe131 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/BackOfficeAuthorizationServerProvider.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin.Security.OAuth; +using Umbraco.Core.Security; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// A simple OAuth server provider to verify back office users + /// + public class BackOfficeAuthorizationServerProvider : OAuthAuthorizationServerProvider + { + public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) + { + var userManager = context.OwinContext.GetUserManager(); + + context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" }); + + var user = await userManager.FindAsync(context.UserName, context.Password); + + if (user == null) + { + context.SetError("invalid_grant", "The user name or password is incorrect."); + return; + } + + var identity = await userManager.ClaimsIdentityFactory.CreateAsync(userManager, user, context.Options.AuthenticationType); + + context.Validated(identity); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e8473c6ce5..df0d56caf7 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -309,6 +309,7 @@ + diff --git a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs index 06d270bf28..4415016c9d 100644 --- a/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs +++ b/src/Umbraco.Web/UmbracoDefaultOwinStartup.cs @@ -1,6 +1,8 @@ using Microsoft.Owin; +using Microsoft.Owin.Logging; using Owin; using Umbraco.Core; +using Umbraco.Core.Logging; using Umbraco.Core.Security; using Umbraco.Web; using Umbraco.Web.Security.Identity; @@ -19,6 +21,8 @@ namespace Umbraco.Web { public virtual void Configuration(IAppBuilder app) { + app.SetUmbracoLoggerFactory(); + //Configure the Identity user manager for use with Umbraco Back office // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation) app.ConfigureUserManagerForUmbracoBackOffice( diff --git a/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs b/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs index 74c830ecbf..82ea6d2d0a 100644 --- a/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/ValidateAngularAntiForgeryTokenAttribute.cs @@ -1,5 +1,7 @@ using System.Net; using System.Net.Http; +using System.Security.Claims; +using System.Web.Http; using System.Web.Http.Filters; namespace Umbraco.Web.WebApi.Filters @@ -10,13 +12,23 @@ namespace Umbraco.Web.WebApi.Filters /// /// Code derived from http://ericpanorel.net/2013/07/28/spa-authentication-and-csrf-mvc4-antiforgery-implementation/ /// - /// TODO: If/when we enable custom authorization (OAuth, or whatever) we'll need to detect that and disable this filter since with custom auth that - /// doesn't come from the same website (cookie), this will always fail. + /// If the authentication type is cookie based, then this filter will execute, otherwise it will be disabled /// public sealed class ValidateAngularAntiForgeryTokenAttribute : ActionFilterAttribute { public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext) { + var userIdentity = ((ApiController) actionContext.ControllerContext.Controller).User.Identity as ClaimsIdentity; + if (userIdentity != null) + { + //if there is not CookiePath claim, then exist + if (userIdentity.HasClaim(x => x.Type == ClaimTypes.CookiePath) == false) + { + base.OnActionExecuting(actionContext); + return; + } + } + string failedReason; if (AngularAntiForgeryHelper.ValidateHeaders(actionContext.Request.Headers, out failedReason) == false) {