Files
Umbraco-CMS/src/Umbraco.Core/Security/UmbracoBackOfficeIdentity.cs

319 lines
13 KiB
C#

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
{
/// <summary>
/// A custom user identity for the Umbraco backoffice
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[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);
}
/// <summary>
/// Create a back office identity based on user data
/// </summary>
/// <param name="userdata"></param>
public UmbracoBackOfficeIdentity(UserData userdata)
//This just creates a temp/fake ticket
: base(new FormsAuthenticationTicket(userdata.Username, true, 10))
{
if (userdata == null) throw new ArgumentNullException("userdata");
UserData = userdata;
AddUserDataClaims();
}
/// <summary>
/// Create a back office identity based on an existing claims identity
/// </summary>
/// <param name="claimsIdentity"></param>
/// <param name="userdata"></param>
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");
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;
AddExistingClaims(claimsIdentity);
Actor = claimsIdentity;
AddUserDataClaims();
}
/// <summary>
/// Create a new identity from a forms auth ticket
/// </summary>
/// <param name="ticket"></param>
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<UserData>(ticket.UserData);
AddUserDataClaims();
}
/// <summary>
/// Used for cloning
/// </summary>
/// <param name="identity"></param>
private UmbracoBackOfficeIdentity(UmbracoBackOfficeIdentity identity)
: base(identity)
{
if (identity.Actor != null)
{
_currentIssuer = identity.AuthenticationType;
AddExistingClaims(identity);
Actor = identity.Clone();
}
UserData = identity.UserData;
AddUserDataClaims();
}
public const string Issuer = "UmbracoBackOffice";
private readonly string _currentIssuer = Issuer;
/// <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)
{
//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);
}
}
/// <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>
/// Adds claims based on the UserData data
/// </summary>
private void AddUserDataClaims()
{
//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 && SessionId.IsNullOrWhiteSpace() == false)
{
AddClaim(new Claim(Constants.Security.SessionIdClaimType, SessionId, ClaimValueTypes.String, Issuer, Issuer, this));
//The security stamp claim is also required... this is because this claim type is hard coded
// by the SecurityStampValidator, see: https://katanaproject.codeplex.com/workitem/444
if (HasClaim(x => x.Type == Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType) == false)
{
AddClaim(new Claim(Microsoft.AspNet.Identity.Constants.DefaultSecurityStampClaimType, SessionId, ClaimValueTypes.String, Issuer, Issuer, this));
}
}
//Add each app as a separate claim
if (HasClaim(x => x.Type == Constants.Security.AllowedApplicationsClaimType) == false)
{
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));
}
}
}
protected internal UserData UserData { get; private set; }
/// <summary>
/// Gets the type of authenticated identity.
/// </summary>
/// <returns>
/// The type of authenticated identity. This property always returns "UmbracoBackOffice".
/// </returns>
public override string AuthenticationType
{
get { return _currentIssuer; }
}
public int StartContentNode
{
get { return UserData.StartContentNode; }
}
public int StartMediaNode
{
get { return UserData.StartMediaNode; }
}
public string[] AllowedApplications
{
get { return UserData.AllowedApplications; }
}
public object Id
{
get { return UserData.Id; }
}
public string RealName
{
get { return UserData.RealName; }
}
public string Username
{
get { return UserData.Username; }
}
public string Culture
{
get { return UserData.Culture; }
}
public string SessionId
{
get { return UserData.SessionId; }
}
public string[] Roles
{
get { return UserData.Roles; }
}
/// <summary>
/// Gets a copy of the current <see cref="T:UmbracoBackOfficeIdentity"/> instance.
/// </summary>
/// <returns>
/// A copy of the current <see cref="T:UmbracoBackOfficeIdentity"/> instance.
/// </returns>
public override ClaimsIdentity Clone()
{
return new UmbracoBackOfficeIdentity(this);
}
}
}