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.

This commit is contained in:
Shannon
2015-04-10 14:22:09 +10:00
parent d6589960f2
commit f2e319a01f
15 changed files with 519 additions and 46 deletions

View File

@@ -37,9 +37,7 @@
public const string StartContentNodeIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/startcontentnode"; 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 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 AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp";
//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"; public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid";
} }

View File

@@ -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> _type;
public OwinLogger(ILogger logger, Lazy<Type> type)
{
_logger = logger;
_type = type;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="eventType"/><param name="eventId"/><param name="state"/><param name="exception"/><param name="formatter"/>
/// <returns/>
public bool WriteCore(TraceEventType eventType, int eventId, object state, Exception exception, Func<object, Exception, string> 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");
}
}
}
}

View File

@@ -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
{
/// <summary>
/// Creates a new ILogger instance of the given name.
/// </summary>
/// <param name="name"/>
/// <returns/>
public Microsoft.Owin.Logging.ILogger Create(string name)
{
return new OwinLogger(
LoggerResolver.HasCurrent ? LoggerResolver.Current.Logger : new DebugDiagnosticsLogger(),
new Lazy<Type>(() => Type.GetType(name) ?? typeof (OwinLogger)));
}
}
}

View File

@@ -5,6 +5,7 @@ using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Security.Claims;
using System.Security.Principal; using System.Security.Principal;
using System.Threading; using System.Threading;
using System.Web; using System.Web;
@@ -14,6 +15,7 @@ using Microsoft.Owin;
using Newtonsoft.Json; using Newtonsoft.Json;
using Umbraco.Core.Configuration; using Umbraco.Core.Configuration;
using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Membership;
using Microsoft.Owin;
namespace Umbraco.Core.Security namespace Umbraco.Core.Security
{ {
@@ -95,8 +97,14 @@ namespace Umbraco.Core.Security
{ {
if (http == null) throw new ArgumentNullException("http"); if (http == null) throw new ArgumentNullException("http");
if (http.User == null) return null; //there's no user at all so no identity 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; if (authenticateRequestIfNotFound == false) return null;

View File

@@ -1,4 +1,5 @@
using System.Linq; using System;
using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity;
@@ -17,17 +18,19 @@ namespace Umbraco.Core.Security
{ {
var baseIdentity = await base.CreateAsync(manager, user, authenticationType); var baseIdentity = await base.CreateAsync(manager, user, authenticationType);
var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity, new UserData() var umbracoIdentity = new UmbracoBackOfficeIdentity(baseIdentity,
{ //set a new session id
Id = user.Id, new UserData(Guid.NewGuid().ToString("N"))
Username = user.UserName, {
RealName = user.Name, Id = user.Id,
AllowedApplications = user.AllowedSections, Username = user.UserName,
Culture = user.Culture, RealName = user.Name,
Roles = user.Roles.Select(x => x.RoleId).ToArray(), AllowedApplications = user.AllowedSections,
StartContentNode = user.StartContentId, Culture = user.Culture,
StartMediaNode = user.StartMediaId Roles = user.Roles.Select(x => x.RoleId).ToArray(),
}); StartContentNode = user.StartContentId,
StartMediaNode = user.StartMediaId
});
return umbracoIdentity; return umbracoIdentity;
} }

View File

@@ -1,10 +1,14 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Security.Claims; using System.Security.Claims;
using System.Security.Principal; using System.Security.Principal;
using System.Web; using System.Web;
using System.Web.Security; using System.Web.Security;
using Microsoft.AspNet.Identity;
using Newtonsoft.Json; using Newtonsoft.Json;
using Umbraco.Core.Configuration;
namespace Umbraco.Core.Security namespace Umbraco.Core.Security
{ {
@@ -19,6 +23,57 @@ namespace Umbraco.Core.Security
[Serializable] [Serializable]
public class UmbracoBackOfficeIdentity : FormsIdentity 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);
}
/// <summary> /// <summary>
/// Create a back office identity based on user data /// Create a back office identity based on user data
/// </summary> /// </summary>
@@ -44,9 +99,15 @@ namespace Umbraco.Core.Security
if (claimsIdentity == null) throw new ArgumentNullException("claimsIdentity"); if (claimsIdentity == null) throw new ArgumentNullException("claimsIdentity");
if (userdata == null) throw new ArgumentNullException("userdata"); 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; _currentIssuer = claimsIdentity.AuthenticationType;
UserData = userdata; UserData = userdata;
AddClaims(claimsIdentity); AddExistingClaims(claimsIdentity);
Actor = claimsIdentity; Actor = claimsIdentity;
AddUserDataClaims(); AddUserDataClaims();
} }
@@ -58,6 +119,9 @@ namespace Umbraco.Core.Security
public UmbracoBackOfficeIdentity(FormsAuthenticationTicket ticket) public UmbracoBackOfficeIdentity(FormsAuthenticationTicket ticket)
: base(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<UserData>(ticket.UserData); UserData = JsonConvert.DeserializeObject<UserData>(ticket.UserData);
AddUserDataClaims(); AddUserDataClaims();
} }
@@ -72,7 +136,7 @@ namespace Umbraco.Core.Security
if (identity.Actor != null) if (identity.Actor != null)
{ {
_currentIssuer = identity.AuthenticationType; _currentIssuer = identity.AuthenticationType;
AddClaims(identity); AddExistingClaims(identity);
Actor = identity.Clone(); Actor = identity.Clone();
} }
@@ -83,43 +147,98 @@ namespace Umbraco.Core.Security
public const string Issuer = "UmbracoBackOffice"; public const string Issuer = "UmbracoBackOffice";
private readonly string _currentIssuer = Issuer; private readonly string _currentIssuer = Issuer;
private void AddClaims(ClaimsIdentity claimsIdentity) /// <summary>
/// Used during ctor to add existing claims from an existing ClaimsIdentity
/// </summary>
/// <param name="claimsIdentity"></param>
private void AddExistingClaims(ClaimsIdentity claimsIdentity)
{ {
foreach (var claim in claimsIdentity.Claims) 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); AddClaim(claim);
} }
} }
/// <summary>
/// Returns the required claim types for a back office identity
/// </summary>
/// <remarks>
/// This does not incude the role claim type or allowed apps type since that is a collection and in theory could be empty
/// </remarks>
public static IEnumerable<string> RequiredBackOfficeIdentityClaimTypes
{
get
{
return new[]
{
ClaimTypes.NameIdentifier, //id
ClaimTypes.Name, //username
ClaimTypes.GivenName,
Constants.Security.StartContentNodeIdClaimType,
Constants.Security.StartMediaNodeIdClaimType,
ClaimTypes.Locality,
Constants.Security.SessionIdClaimType
};
}
}
/// <summary> /// <summary>
/// Adds claims based on the UserData data /// Adds claims based on the UserData data
/// </summary> /// </summary>
private void AddUserDataClaims() 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 foreach (var application in AllowedApplications)
new Claim(ClaimTypes.NameIdentifier, Id.ToString(), null, Issuer, Issuer, this), {
AddClaim(new Claim(Constants.Security.AllowedApplicationsClaimType, application, ClaimValueTypes.String, 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<TUser, TKey> not sure we need this
new Claim(Constants.Security.CultureClaimType, Culture, null, Issuer, Issuer, this)
//TODO: Role claims are added by the default ClaimsIdentityFactory<TUser, TKey> 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));
} }
//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; } get { return UserData.RealName; }
} }
public string Username
{
get { return UserData.Username; }
}
public string Culture public string Culture
{ {
get { return UserData.Culture; } get { return UserData.Culture; }

View File

@@ -345,6 +345,8 @@
<Compile Include="HideFromTypeFinderAttribute.cs" /> <Compile Include="HideFromTypeFinderAttribute.cs" />
<Compile Include="IApplicationEventHandler.cs" /> <Compile Include="IApplicationEventHandler.cs" />
<Compile Include="IDisposeOnRequestEnd.cs" /> <Compile Include="IDisposeOnRequestEnd.cs" />
<Compile Include="Logging\OwinLogger.cs" />
<Compile Include="Logging\OwinLoggerFactory.cs" />
<Compile Include="Manifest\GridEditorConverter.cs" /> <Compile Include="Manifest\GridEditorConverter.cs" />
<Compile Include="Models\AuditItem.cs" /> <Compile Include="Models\AuditItem.cs" />
<Compile Include="Models\AuditType.cs" /> <Compile Include="Models\AuditType.cs" />

View File

@@ -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<InvalidOperationException>(() => 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<InvalidOperationException>(() => 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());
}
}
}

View File

@@ -181,6 +181,7 @@
<Compile Include="Persistence\Repositories\TaskTypeRepositoryTest.cs" /> <Compile Include="Persistence\Repositories\TaskTypeRepositoryTest.cs" />
<Compile Include="Resolvers\ResolverBaseTest.cs" /> <Compile Include="Resolvers\ResolverBaseTest.cs" />
<Compile Include="Routing\UrlRoutingTestBase.cs" /> <Compile Include="Routing\UrlRoutingTestBase.cs" />
<Compile Include="Security\UmbracoBackOfficeIdentityTests.cs" />
<Compile Include="StringNewlineExtensions.cs" /> <Compile Include="StringNewlineExtensions.cs" />
<Compile Include="Strings\StylesheetHelperTests.cs" /> <Compile Include="Strings\StylesheetHelperTests.cs" />
<Compile Include="Strings\StringValidationTests.cs" /> <Compile Include="Strings\StringValidationTests.cs" />

View File

@@ -93,7 +93,7 @@ namespace Umbraco.Web.Editors
{ {
var cultureInfo = culture == null var cultureInfo = culture == null
//if the user is logged in, get their culture, otherwise default to 'en' //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) ? Security.CurrentUser.GetUserCulture(Services.TextService)
: CultureInfo.GetCultureInfo("en") : CultureInfo.GetCultureInfo("en")
: CultureInfo.GetCultureInfo(culture); : CultureInfo.GetCultureInfo(culture);

View File

@@ -6,6 +6,7 @@ using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin; using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin; using Microsoft.Owin;
using Microsoft.Owin.Extensions; using Microsoft.Owin.Extensions;
using Microsoft.Owin.Logging;
using Microsoft.Owin.Security; using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies; using Microsoft.Owin.Security.Cookies;
using Owin; using Owin;
@@ -20,6 +21,15 @@ namespace Umbraco.Web.Security.Identity
{ {
public static class AppBuilderExtensions public static class AppBuilderExtensions
{ {
/// <summary>
/// Sets the OWIN logger to use Umbraco's logging system
/// </summary>
/// <param name="app"></param>
public static void SetUmbracoLoggerFactory(this IAppBuilder app)
{
app.SetLoggerFactory(new OwinLoggerFactory());
}
#region Backoffice #region Backoffice
/// <summary> /// <summary>
@@ -112,7 +122,7 @@ namespace Umbraco.Web.Security.Identity
GlobalSettings.UseSSL) GlobalSettings.UseSSL)
{ {
Provider = new CookieAuthenticationProvider Provider = new CookieAuthenticationProvider
{ {
// Enables the application to validate the security stamp when the user // Enables the application to validate the security stamp when the user
// logs in. This is a security feature which is used when you // logs in. This is a security feature which is used when you
// change a password or add an external login to your account. // change a password or add an external login to your account.

View File

@@ -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
{
/// <summary>
/// A simple OAuth server provider to verify back office users
/// </summary>
public class BackOfficeAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var userManager = context.OwinContext.GetUserManager<BackOfficeUserManager>();
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);
}
}
}

View File

@@ -309,6 +309,7 @@
<Compile Include="ApplicationContextExtensions.cs" /> <Compile Include="ApplicationContextExtensions.cs" />
<Compile Include="AreaRegistrationContextExtensions.cs" /> <Compile Include="AreaRegistrationContextExtensions.cs" />
<Compile Include="HtmlHelperBackOfficeExtensions.cs" /> <Compile Include="HtmlHelperBackOfficeExtensions.cs" />
<Compile Include="Security\Identity\BackOfficeAuthorizationServerProvider.cs" />
<Compile Include="UmbracoDefaultOwinStartup.cs" /> <Compile Include="UmbracoDefaultOwinStartup.cs" />
<Compile Include="IUmbracoContextAccessor.cs" /> <Compile Include="IUmbracoContextAccessor.cs" />
<Compile Include="Models\ContentEditing\Relation.cs" /> <Compile Include="Models\ContentEditing\Relation.cs" />

View File

@@ -1,6 +1,8 @@
using Microsoft.Owin; using Microsoft.Owin;
using Microsoft.Owin.Logging;
using Owin; using Owin;
using Umbraco.Core; using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Security; using Umbraco.Core.Security;
using Umbraco.Web; using Umbraco.Web;
using Umbraco.Web.Security.Identity; using Umbraco.Web.Security.Identity;
@@ -19,6 +21,8 @@ namespace Umbraco.Web
{ {
public virtual void Configuration(IAppBuilder app) public virtual void Configuration(IAppBuilder app)
{ {
app.SetUmbracoLoggerFactory();
//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) // (EXPERT: an overload accepts a custom BackOfficeUserStore implementation)
app.ConfigureUserManagerForUmbracoBackOffice( app.ConfigureUserManagerForUmbracoBackOffice(

View File

@@ -1,5 +1,7 @@
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Security.Claims;
using System.Web.Http;
using System.Web.Http.Filters; using System.Web.Http.Filters;
namespace Umbraco.Web.WebApi.Filters namespace Umbraco.Web.WebApi.Filters
@@ -10,13 +12,23 @@ namespace Umbraco.Web.WebApi.Filters
/// <remarks> /// <remarks>
/// Code derived from http://ericpanorel.net/2013/07/28/spa-authentication-and-csrf-mvc4-antiforgery-implementation/ /// 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 /// If the authentication type is cookie based, then this filter will execute, otherwise it will be disabled
/// doesn't come from the same website (cookie), this will always fail.
/// </remarks> /// </remarks>
public sealed class ValidateAngularAntiForgeryTokenAttribute : ActionFilterAttribute public sealed class ValidateAngularAntiForgeryTokenAttribute : ActionFilterAttribute
{ {
public override void OnActionExecuting(System.Web.Http.Controllers.HttpActionContext actionContext) 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; string failedReason;
if (AngularAntiForgeryHelper.ValidateHeaders(actionContext.Request.Headers, out failedReason) == false) if (AngularAntiForgeryHelper.ValidateHeaders(actionContext.Request.Headers, out failedReason) == false)
{ {