2013-08-12 15:06:12 +02:00
using System ;
2015-04-10 14:22:09 +10:00
using System.Collections.Generic ;
using System.Linq ;
2013-10-25 11:35:09 +11:00
using System.Runtime.Serialization ;
2015-02-06 16:13:02 +11:00
using System.Security.Claims ;
using System.Security.Principal ;
2013-08-12 15:06:12 +02:00
using System.Web ;
using System.Web.Security ;
2015-04-10 14:22:09 +10:00
using Microsoft.AspNet.Identity ;
2013-08-12 15:06:12 +02:00
using Newtonsoft.Json ;
2015-04-10 14:22:09 +10:00
using Umbraco.Core.Configuration ;
2013-08-12 15:06:12 +02:00
namespace Umbraco.Core.Security
{
2015-02-06 16:13:02 +11:00
2013-08-12 15:06:12 +02:00
/// <summary>
/// A custom user identity for the Umbraco backoffice
/// </summary>
/// <remarks>
2015-02-06 16:13:02 +11:00
/// 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.
2013-08-12 15:06:12 +02:00
/// </remarks>
2013-10-25 11:35:09 +11:00
[Serializable]
2013-08-12 15:06:12 +02:00
public class UmbracoBackOfficeIdentity : FormsIdentity
{
2015-04-10 14:22:09 +10:00
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 ) ;
}
2015-02-06 16:13:02 +11:00
/// <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 ) )
{
2015-02-09 17:37:21 +11:00
if ( userdata = = null ) throw new ArgumentNullException ( "userdata" ) ;
2015-02-06 16:13:02 +11:00
UserData = userdata ;
2015-02-09 17:37:21 +11:00
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" ) ;
2015-04-10 14:22:09 +10:00
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 ) ) ;
}
2015-02-09 17:37:21 +11:00
_currentIssuer = claimsIdentity . AuthenticationType ;
UserData = userdata ;
2015-04-10 14:22:09 +10:00
AddExistingClaims ( claimsIdentity ) ;
2015-02-09 17:37:21 +11:00
Actor = claimsIdentity ;
AddUserDataClaims ( ) ;
2015-02-06 16:13:02 +11:00
}
/// <summary>
/// Create a new identity from a forms auth ticket
/// </summary>
/// <param name="ticket"></param>
public UmbracoBackOfficeIdentity ( FormsAuthenticationTicket ticket )
2013-08-12 15:06:12 +02:00
: base ( ticket )
{
2015-04-10 14:22:09 +10:00
//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 ) ) ;
2015-02-06 16:13:02 +11:00
UserData = JsonConvert . DeserializeObject < UserData > ( ticket . UserData ) ;
2015-02-09 17:37:21 +11:00
AddUserDataClaims ( ) ;
2013-08-12 15:06:12 +02:00
}
2015-02-06 16:13:02 +11:00
/// <summary>
/// Used for cloning
/// </summary>
/// <param name="identity"></param>
private UmbracoBackOfficeIdentity ( UmbracoBackOfficeIdentity identity )
: base ( identity )
{
2015-02-09 17:37:21 +11:00
if ( identity . Actor ! = null )
{
_currentIssuer = identity . AuthenticationType ;
2015-04-10 14:22:09 +10:00
AddExistingClaims ( identity ) ;
2015-02-09 17:37:21 +11:00
Actor = identity . Clone ( ) ;
}
2015-02-06 16:13:02 +11:00
UserData = identity . UserData ;
2015-02-09 17:37:21 +11:00
AddUserDataClaims ( ) ;
2015-02-06 16:13:02 +11:00
}
2015-02-09 17:37:21 +11:00
public const string Issuer = "UmbracoBackOffice" ;
private readonly string _currentIssuer = Issuer ;
2015-04-10 14:22:09 +10:00
/// <summary>
/// Used during ctor to add existing claims from an existing ClaimsIdentity
/// </summary>
/// <param name="claimsIdentity"></param>
private void AddExistingClaims ( ClaimsIdentity claimsIdentity )
2015-02-09 17:37:21 +11:00
{
foreach ( var claim in claimsIdentity . Claims )
{
2015-04-10 14:22:09 +10:00
//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" ) ) ;
2015-02-09 17:37:21 +11:00
AddClaim ( claim ) ;
}
}
2015-02-06 16:13:02 +11:00
2015-04-10 14:22:09 +10:00
/// <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
} ;
}
}
2015-02-09 17:37:21 +11:00
/// <summary>
/// Adds claims based on the UserData data
/// </summary>
private void AddUserDataClaims ( )
2013-08-12 15:06:12 +02:00
{
2015-04-10 14:22:09 +10:00
//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 ) ) ;
2015-02-20 14:17:28 +01:00
2015-04-10 14:22:09 +10:00
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 ) ) ;
2015-02-09 17:37:21 +11:00
2015-04-10 14:22:09 +10:00
if ( HasClaim ( x = > x . Type = = Constants . Security . StartContentNodeIdClaimType ) = = false )
AddClaim ( new Claim ( Constants . Security . StartContentNodeIdClaimType , StartContentNode . ToInvariantString ( ) , ClaimValueTypes . Integer32 , Issuer , Issuer , this ) ) ;
2015-02-09 17:37:21 +11:00
2015-04-10 14:22:09 +10:00
if ( HasClaim ( x = > x . Type = = Constants . Security . StartMediaNodeIdClaimType ) = = false )
AddClaim ( new Claim ( Constants . Security . StartMediaNodeIdClaimType , StartMediaNode . ToInvariantString ( ) , ClaimValueTypes . Integer32 , Issuer , Issuer , this ) ) ;
2015-02-20 14:17:28 +01:00
2015-04-10 14:22:09 +10:00
if ( HasClaim ( x = > x . Type = = ClaimTypes . Locality ) = = false )
AddClaim ( new Claim ( ClaimTypes . Locality , Culture , ClaimValueTypes . String , Issuer , Issuer , this ) ) ;
2016-02-02 18:01:36 +01:00
if ( HasClaim ( x = > x . Type = = Constants . Security . SessionIdClaimType ) = = false & & SessionId . IsNullOrWhiteSpace ( ) = = false )
2016-02-02 15:14:47 +01:00
{
2015-04-10 14:22:09 +10:00
AddClaim ( new Claim ( Constants . Security . SessionIdClaimType , SessionId , ClaimValueTypes . String , Issuer , Issuer , this ) ) ;
2016-02-02 15:14:47 +01:00
//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 ) ) ;
}
}
2015-04-10 14:22:09 +10:00
//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 )
2015-02-20 14:17:28 +01:00
{
2015-04-10 14:22:09 +10:00
//manually add them based on the UserData
foreach ( var roleName in UserData . Roles )
{
AddClaim ( new Claim ( RoleClaimType , roleName , ClaimValueTypes . String , Issuer , Issuer , this ) ) ;
}
2015-02-20 14:17:28 +01:00
}
2015-04-10 14:22:09 +10:00
2015-04-15 20:38:04 +10:00
2015-02-20 14:17:28 +01:00
2015-02-06 16:13:02 +11:00
}
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
{
2015-02-09 17:37:21 +11:00
get { return _currentIssuer ; }
2015-02-06 16:13:02 +11:00
}
public int StartContentNode
{
get { return UserData . StartContentNode ; }
2013-08-12 15:06:12 +02:00
}
public int StartMediaNode
{
2015-02-06 16:13:02 +11:00
get { return UserData . StartMediaNode ; }
2013-08-12 15:06:12 +02:00
}
public string [ ] AllowedApplications
{
2015-02-06 16:13:02 +11:00
get { return UserData . AllowedApplications ; }
2013-08-12 15:06:12 +02:00
}
2015-02-06 16:13:02 +11:00
2013-08-12 15:06:12 +02:00
public object Id
{
2015-02-06 16:13:02 +11:00
get { return UserData . Id ; }
2013-08-12 15:06:12 +02:00
}
public string RealName
{
2015-02-06 16:13:02 +11:00
get { return UserData . RealName ; }
2013-08-12 15:06:12 +02:00
}
2015-04-10 14:22:09 +10:00
public string Username
{
get { return UserData . Username ; }
}
2013-08-12 15:06:12 +02:00
public string Culture
{
2015-02-06 16:13:02 +11:00
get { return UserData . Culture ; }
2013-08-12 15:06:12 +02:00
}
2013-11-29 12:42:50 +11:00
public string SessionId
{
2015-02-06 16:13:02 +11:00
get { return UserData . SessionId ; }
2013-11-29 12:42:50 +11:00
}
2013-08-12 15:06:12 +02:00
public string [ ] Roles
{
2015-02-06 16:13:02 +11:00
get { return UserData . Roles ; }
2013-08-12 15:06:12 +02:00
}
/// <summary>
2015-02-06 16:13:02 +11:00
/// Gets a copy of the current <see cref="T:UmbracoBackOfficeIdentity"/> instance.
2013-08-12 15:06:12 +02:00
/// </summary>
2015-02-06 16:13:02 +11:00
/// <returns>
/// A copy of the current <see cref="T:UmbracoBackOfficeIdentity"/> instance.
/// </returns>
public override ClaimsIdentity Clone ( )
2013-08-12 15:06:12 +02:00
{
2015-02-06 16:13:02 +11:00
return new UmbracoBackOfficeIdentity ( this ) ;
2013-08-12 15:06:12 +02:00
}
2013-10-25 11:35:09 +11:00
2013-08-12 15:06:12 +02:00
}
2013-07-31 17:08:56 +10:00
}