File: System\Security\Claims\ClaimsIdentity.cs
Web Access
Project: src\src\libraries\System.Security.Claims\src\System.Security.Claims.csproj (System.Security.Claims)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization;
using System.Security.Principal;
 
namespace System.Security.Claims
{
    /// <summary>
    /// An Identity that is represented by a set of claims.
    /// </summary>
    [DebuggerDisplay("{DebuggerToString(),nq}")]
    public class ClaimsIdentity : IIdentity
    {
        private enum SerializationMask
        {
            None = 0,
            AuthenticationType = 1,
            BootstrapConext = 2,
            NameClaimType = 4,
            RoleClaimType = 8,
            HasClaims = 16,
            HasLabel = 32,
            Actor = 64,
            UserData = 128,
        }
 
        private byte[]? _userSerializationData;
        private ClaimsIdentity? _actor;
        private string? _authenticationType;
        private object? _bootstrapContext;
        private List<List<Claim>>? _externalClaims;
        private string? _label;
        private readonly List<Claim> _instanceClaims = new List<Claim>();
        private string _nameClaimType = DefaultNameClaimType;
        private string _roleClaimType = DefaultRoleClaimType;
 
        public const string DefaultIssuer = @"LOCAL AUTHORITY";
        public const string DefaultNameClaimType = ClaimTypes.Name;
        public const string DefaultRoleClaimType = ClaimTypes.Role;
 
        // NOTE about _externalClaims.
        // GenericPrincpal and RolePrincipal set role claims here so that .IsInRole will be consistent with a 'role' claim found by querying the identity or principal.
        // _externalClaims are external to the identity and assumed to be dynamic, they not serialized or copied through Clone().
        // Access through public method: ClaimProviders.
 
        /// <summary>
        /// Initializes an instance of <see cref="ClaimsIdentity"/>.
        /// </summary>
        public ClaimsIdentity()
            : this((IIdentity?)null, (IEnumerable<Claim>?)null, (string?)null, (string?)null, (string?)null)
        {
        }
 
        /// <summary>
        /// Initializes an instance of <see cref="ClaimsIdentity"/>.
        /// </summary>
        /// <param name="identity"><see cref="IIdentity"/> supplies the <see cref="Name"/> and <see cref="AuthenticationType"/>.</param>
        /// <remarks><seealso cref="ClaimsIdentity(IIdentity, IEnumerable{Claim}, string, string, string)"/> for details on how internal values are set.</remarks>
        public ClaimsIdentity(IIdentity? identity)
            : this(identity, (IEnumerable<Claim>?)null, (string?)null, (string?)null, (string?)null)
        {
        }
 
        /// <summary>
        /// Initializes an instance of <see cref="ClaimsIdentity"/>.
        /// </summary>
        /// <param name="claims"><see cref="IEnumerable{Claim}"/> associated with this instance.</param>
        /// <remarks>
        /// <remarks><seealso cref="ClaimsIdentity(IIdentity, IEnumerable{Claim}, string, string, string)"/> for details on how internal values are set.</remarks>
        /// </remarks>
        public ClaimsIdentity(IEnumerable<Claim>? claims)
            : this((IIdentity?)null, claims, (string?)null, (string?)null, (string?)null)
        {
        }
 
        /// <summary>
        /// Initializes an instance of <see cref="ClaimsIdentity"/>.
        /// </summary>
        /// <param name="authenticationType">The authentication method used to establish this identity.</param>
        public ClaimsIdentity(string? authenticationType)
            : this((IIdentity?)null, (IEnumerable<Claim>?)null, authenticationType, (string?)null, (string?)null)
        {
        }
 
        /// <summary>
        /// Initializes an instance of <see cref="ClaimsIdentity"/>.
        /// </summary>
        /// <param name="claims"><see cref="IEnumerable{Claim}"/> associated with this instance.</param>
        /// <param name="authenticationType">The authentication method used to establish this identity.</param>
        /// <remarks><seealso cref="ClaimsIdentity(IIdentity, IEnumerable{Claim}, string, string, string)"/> for details on how internal values are set.</remarks>
        public ClaimsIdentity(IEnumerable<Claim>? claims, string? authenticationType)
            : this((IIdentity?)null, claims, authenticationType, (string?)null, (string?)null)
        {
        }
 
        /// <summary>
        /// Initializes an instance of <see cref="ClaimsIdentity"/>.
        /// </summary>
        /// <param name="identity"><see cref="IIdentity"/> supplies the <see cref="Name"/> and <see cref="AuthenticationType"/>.</param>
        /// <param name="claims"><see cref="IEnumerable{Claim}"/> associated with this instance.</param>
        /// <remarks><seealso cref="ClaimsIdentity(IIdentity, IEnumerable{Claim}, string, string, string)"/> for details on how internal values are set.</remarks>
        public ClaimsIdentity(IIdentity? identity, IEnumerable<Claim>? claims)
            : this(identity, claims, (string?)null, (string?)null, (string?)null)
        {
        }
 
        /// <summary>
        /// Initializes an instance of <see cref="ClaimsIdentity"/>.
        /// </summary>
        /// <param name="authenticationType">The type of authentication used.</param>
        /// <param name="nameType">The <see cref="Claim.Type"/> used when obtaining the value of <see cref="ClaimsIdentity.Name"/>.</param>
        /// <param name="roleType">The <see cref="Claim.Type"/> used when performing logic for <see cref="ClaimsPrincipal.IsInRole"/>.</param>
        /// <remarks><seealso cref="ClaimsIdentity(IIdentity, IEnumerable{Claim}, string, string, string)"/> for details on how internal values are set.</remarks>
        public ClaimsIdentity(string? authenticationType, string? nameType, string? roleType)
            : this((IIdentity?)null, (IEnumerable<Claim>?)null, authenticationType, nameType, roleType)
        {
        }
 
        /// <summary>
        /// Initializes an instance of <see cref="ClaimsIdentity"/>.
        /// </summary>
        /// <param name="claims"><see cref="IEnumerable{Claim}"/> associated with this instance.</param>
        /// <param name="authenticationType">The type of authentication used.</param>
        /// <param name="nameType">The <see cref="Claim.Type"/> used when obtaining the value of <see cref="ClaimsIdentity.Name"/>.</param>
        /// <param name="roleType">The <see cref="Claim.Type"/> used when performing logic for <see cref="ClaimsPrincipal.IsInRole"/>.</param>
        /// <remarks><seealso cref="ClaimsIdentity(IIdentity, IEnumerable{Claim}, string, string, string)"/> for details on how internal values are set.</remarks>
        public ClaimsIdentity(IEnumerable<Claim>? claims, string? authenticationType, string? nameType, string? roleType)
            : this((IIdentity?)null, claims, authenticationType, nameType, roleType)
        {
        }
 
        /// <summary>
        /// Initializes an instance of <see cref="ClaimsIdentity"/>.
        /// </summary>
        /// <param name="identity"><see cref="IIdentity"/> supplies the <see cref="Name"/> and <see cref="AuthenticationType"/>.</param>
        /// <param name="claims"><see cref="IEnumerable{Claim}"/> associated with this instance.</param>
        /// <param name="authenticationType">The type of authentication used.</param>
        /// <param name="nameType">The <see cref="Claim.Type"/> used when obtaining the value of <see cref="ClaimsIdentity.Name"/>.</param>
        /// <param name="roleType">The <see cref="Claim.Type"/> used when performing logic for <see cref="ClaimsPrincipal.IsInRole"/>.</param>
        /// <remarks>If 'identity' is a <see cref="ClaimsIdentity"/>, then there are potentially multiple sources for AuthenticationType, NameClaimType, RoleClaimType.
        /// <para>Priority is given to the parameters: authenticationType, nameClaimType, roleClaimType.</para>
        /// <para>All <see cref="Claim"/>s are copied into this instance in a <see cref="List{Claim}"/>. Each Claim is examined and if Claim.Subject != this, then Claim.Clone(this) is called before the claim is added.</para>
        /// <para>Any 'External' claims are ignored.</para>
        /// </remarks>
        /// <exception cref="InvalidOperationException">if 'identity' is a <see cref="ClaimsIdentity"/> and <see cref="ClaimsIdentity.Actor"/> results in a circular reference back to 'this'.</exception>
        public ClaimsIdentity(IIdentity? identity, IEnumerable<Claim>? claims, string? authenticationType, string? nameType, string? roleType)
        {
            ClaimsIdentity? claimsIdentity = identity as ClaimsIdentity;
 
            _authenticationType = (identity != null && string.IsNullOrEmpty(authenticationType)) ? identity.AuthenticationType : authenticationType;
            _nameClaimType = !string.IsNullOrEmpty(nameType) ? nameType : (claimsIdentity != null ? claimsIdentity._nameClaimType : DefaultNameClaimType);
            _roleClaimType = !string.IsNullOrEmpty(roleType) ? roleType : (claimsIdentity != null ? claimsIdentity._roleClaimType : DefaultRoleClaimType);
 
            if (claimsIdentity != null)
            {
                _label = claimsIdentity._label;
                _bootstrapContext = claimsIdentity._bootstrapContext;
 
                if (claimsIdentity.Actor != null)
                {
                    //
                    // Check if the Actor is circular before copying. That check is done while setting
                    // the Actor property and so not really needed here. But checking just for sanity sake
                    //
                    if (!IsCircular(claimsIdentity.Actor))
                    {
                        _actor = claimsIdentity.Actor;
                    }
                    else
                    {
                        throw new InvalidOperationException(SR.InvalidOperationException_ActorGraphCircular);
                    }
                }
                SafeAddClaims(claimsIdentity._instanceClaims);
            }
            else
            {
                if (identity != null && !string.IsNullOrEmpty(identity.Name))
                {
                    SafeAddClaim(new Claim(_nameClaimType, identity.Name, ClaimValueTypes.String, DefaultIssuer, DefaultIssuer, this));
                }
            }
 
            if (claims != null)
            {
                SafeAddClaims(claims);
            }
        }
 
        /// <summary>
        /// Initializes an instance of <see cref="ClaimsIdentity"/> using a <see cref="BinaryReader"/>.
        /// Normally the <see cref="BinaryReader"/> is constructed using the bytes from <see cref="WriteTo(BinaryWriter)"/> and initialized in the same way as the <see cref="BinaryWriter"/>.
        /// </summary>
        /// <param name="reader">a <see cref="BinaryReader"/> pointing to a <see cref="ClaimsIdentity"/>.</param>
        /// <exception cref="ArgumentNullException">if 'reader' is null.</exception>
        public ClaimsIdentity(BinaryReader reader)
        {
            ArgumentNullException.ThrowIfNull(reader);
 
            Initialize(reader);
        }
 
        /// <summary>
        /// Copy constructor.
        /// </summary>
        /// <param name="other"><see cref="ClaimsIdentity"/> to copy.</param>
        /// <exception cref="ArgumentNullException">if 'other' is null.</exception>
        protected ClaimsIdentity(ClaimsIdentity other)
        {
            ArgumentNullException.ThrowIfNull(other);
 
            if (other._actor != null)
            {
                _actor = other._actor.Clone();
            }
 
            _authenticationType = other._authenticationType;
            _bootstrapContext = other._bootstrapContext;
            _label = other._label;
            _nameClaimType = other._nameClaimType;
            _roleClaimType = other._roleClaimType;
            if (other._userSerializationData != null)
            {
                _userSerializationData = other._userSerializationData.Clone() as byte[];
            }
 
            SafeAddClaims(other._instanceClaims);
        }
 
        [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        protected ClaimsIdentity(SerializationInfo info, StreamingContext context)
        {
            throw new PlatformNotSupportedException();
        }
 
        /// <summary>
        /// Initializes an instance of <see cref="ClaimsIdentity"/> from a serialized stream created via
        /// <see cref="ISerializable"/>.
        /// </summary>
        /// <param name="info">
        /// The <see cref="SerializationInfo"/> to read from.
        /// </param>
        /// <exception cref="ArgumentNullException">Thrown is the <paramref name="info"/> is null.</exception>
        [Obsolete(Obsoletions.LegacyFormatterImplMessage, DiagnosticId = Obsoletions.LegacyFormatterImplDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
        [EditorBrowsable(EditorBrowsableState.Never)]
        protected ClaimsIdentity(SerializationInfo info)
        {
            throw new PlatformNotSupportedException();
        }
 
        /// <summary>
        /// Gets the authentication type that can be used to determine how this <see cref="ClaimsIdentity"/> authenticated to an authority.
        /// </summary>
        public virtual string? AuthenticationType
        {
            get { return _authenticationType; }
        }
 
        /// <summary>
        /// Gets a value that indicates if the user has been authenticated.
        /// </summary>
        public virtual bool IsAuthenticated
        {
            get { return !string.IsNullOrEmpty(_authenticationType); }
        }
 
        /// <summary>
        /// Gets or sets a <see cref="ClaimsIdentity"/> that was granted delegation rights.
        /// </summary>
        /// <exception cref="InvalidOperationException">if 'value' results in a circular reference back to 'this'.</exception>
        public ClaimsIdentity? Actor
        {
            get { return _actor; }
            set
            {
                if (value != null)
                {
                    if (IsCircular(value))
                    {
                        throw new InvalidOperationException(SR.InvalidOperationException_ActorGraphCircular);
                    }
                }
                _actor = value;
            }
        }
 
        /// <summary>
        /// Gets or sets a context that was used to create this <see cref="ClaimsIdentity"/>.
        /// </summary>
        public object? BootstrapContext
        {
            get { return _bootstrapContext; }
            set { _bootstrapContext = value; }
        }
 
        /// <summary>
        /// Gets the claims as <see cref="IEnumerable{Claim}"/>, associated with this <see cref="ClaimsIdentity"/>.
        /// </summary>
        /// <remarks>May contain nulls.</remarks>
        public virtual IEnumerable<Claim> Claims
        {
            get
            {
                if (_externalClaims == null)
                {
                    return _instanceClaims;
                }
 
                return CombinedClaimsIterator();
            }
        }
 
        private IEnumerable<Claim> CombinedClaimsIterator()
        {
            for (int i = 0; i < _instanceClaims.Count; i++)
            {
                yield return _instanceClaims[i];
            }
 
            for (int j = 0; j < _externalClaims!.Count; j++)
            {
                if (_externalClaims[j] != null)
                {
                    foreach (Claim claim in _externalClaims[j])
                    {
                        yield return claim;
                    }
                }
            }
        }
 
        /// <summary>
        /// Contains any additional data provided by a derived type, typically set when calling <see cref="WriteTo(BinaryWriter, byte[])"/>.
        /// </summary>
        protected virtual byte[]? CustomSerializationData
        {
            get
            {
                return _userSerializationData;
            }
        }
 
        /// <summary>
        /// Allow the association of claims with this instance of <see cref="ClaimsIdentity"/>.
        /// The claims will not be serialized or added in Clone(). They will be included in searches, finds and returned from the call to <see cref="ClaimsIdentity.Claims"/>.
        /// </summary>
        internal List<List<Claim>> ExternalClaims => _externalClaims ??= new List<List<Claim>>();
 
        /// <summary>
        /// Gets or sets the label for this <see cref="ClaimsIdentity"/>
        /// </summary>
        public string? Label
        {
            get { return _label; }
            set { _label = value; }
        }
 
        /// <summary>
        /// Gets the Name of this <see cref="ClaimsIdentity"/>.
        /// </summary>
        /// <remarks>Calls <see cref="FindFirst(string)"/> where string == NameClaimType, if found, returns <see cref="Claim.Value"/> otherwise null.</remarks>
        public virtual string? Name
        {
            // just an accessor for getting the name claim
            get
            {
                Claim? claim = FindFirst(_nameClaimType);
                if (claim != null)
                {
                    return claim.Value;
                }
 
                return null;
            }
        }
 
        /// <summary>
        /// Gets the value that identifies 'Name' claims. This is used when returning the property <see cref="ClaimsIdentity.Name"/>.
        /// </summary>
        public string NameClaimType
        {
            get { return _nameClaimType; }
        }
 
        /// <summary>
        /// Gets the value that identifies 'Role' claims. This is used when calling <see cref="ClaimsPrincipal.IsInRole"/>.
        /// </summary>
        public string RoleClaimType
        {
            get { return _roleClaimType; }
        }
 
        /// <summary>
        /// Creates a new instance of <see cref="ClaimsIdentity"/> with values copied from this object.
        /// </summary>
        public virtual ClaimsIdentity Clone()
        {
            return new ClaimsIdentity(this);
        }
 
        /// <summary>
        /// Adds a single <see cref="Claim"/> to an internal list.
        /// </summary>
        /// <param name="claim">the <see cref="Claim"/>add.</param>
        /// <remarks>If <see cref="Claim.Subject"/> != this, then Claim.Clone(this) is called before the claim is added.</remarks>
        /// <exception cref="ArgumentNullException">if 'claim' is null.</exception>
        public virtual void AddClaim(Claim claim)
        {
            ArgumentNullException.ThrowIfNull(claim);
 
            if (object.ReferenceEquals(claim.Subject, this))
            {
                _instanceClaims.Add(claim);
            }
            else
            {
                _instanceClaims.Add(claim.Clone(this));
            }
        }
 
        /// <summary>
        /// Adds a <see cref="IEnumerable{Claim}"/> to the internal list.
        /// </summary>
        /// <param name="claims">Enumeration of claims to add.</param>
        /// <remarks>Each claim is examined and if <see cref="Claim.Subject"/> != this, then Claim.Clone(this) is called before the claim is added.</remarks>
        /// <exception cref="ArgumentNullException">if 'claims' is null.</exception>
        public virtual void AddClaims(IEnumerable<Claim?> claims)
        {
            ArgumentNullException.ThrowIfNull(claims);
 
            foreach (Claim? claim in claims)
            {
                if (claim == null)
                {
                    continue;
                }
 
                if (object.ReferenceEquals(claim.Subject, this))
                {
                    _instanceClaims.Add(claim);
                }
                else
                {
                    _instanceClaims.Add(claim.Clone(this));
                }
            }
        }
 
        /// <summary>
        /// Attempts to remove a <see cref="Claim"/> the internal list.
        /// </summary>
        /// <param name="claim">the <see cref="Claim"/> to match.</param>
        /// <remarks> It is possible that a <see cref="Claim"/> returned from <see cref="Claims"/> cannot be removed. This would be the case for 'External' claims that are provided by reference.
        /// <para>object.ReferenceEquals is used to 'match'.</para>
        /// </remarks>
        public virtual bool TryRemoveClaim(Claim? claim)
        {
            if (claim == null)
            {
                return false;
            }
 
            bool removed = false;
 
            for (int i = 0; i < _instanceClaims.Count; i++)
            {
                if (object.ReferenceEquals(_instanceClaims[i], claim))
                {
                    _instanceClaims.RemoveAt(i);
                    removed = true;
                    break;
                }
            }
            return removed;
        }
 
        /// <summary>
        /// Removes a <see cref="Claim"/> from the internal list.
        /// </summary>
        /// <param name="claim">the <see cref="Claim"/> to match.</param>
        /// <remarks> It is possible that a <see cref="Claim"/> returned from <see cref="Claims"/> cannot be removed. This would be the case for 'External' claims that are provided by reference.
        /// <para>object.ReferenceEquals is used to 'match'.</para>
        /// </remarks>
        /// <exception cref="InvalidOperationException">if 'claim' cannot be removed.</exception>
        public virtual void RemoveClaim(Claim? claim)
        {
            if (!TryRemoveClaim(claim))
            {
                throw new InvalidOperationException(SR.Format(SR.InvalidOperation_ClaimCannotBeRemoved, claim));
            }
        }
 
        /// <summary>
        /// Adds claims to internal list. Calling Claim.Clone if Claim.Subject != this.
        /// </summary>
        /// <param name="claims">a <see cref="IEnumerable{Claim}"/> to add to </param>
        /// <remarks>private only call from constructor, adds to internal list.</remarks>
        private void SafeAddClaims(IEnumerable<Claim?> claims)
        {
            foreach (Claim? claim in claims)
            {
                if (claim == null)
                    continue;
 
                if (object.ReferenceEquals(claim.Subject, this))
                {
                    _instanceClaims.Add(claim);
                }
                else
                {
                    _instanceClaims.Add(claim.Clone(this));
                }
            }
        }
 
        /// <summary>
        /// Adds claim to internal list. Calling Claim.Clone if Claim.Subject != this.
        /// </summary>
        /// <remarks>private only call from constructor, adds to internal list.</remarks>
        private void SafeAddClaim(Claim? claim)
        {
            if (claim == null)
                return;
 
            if (object.ReferenceEquals(claim.Subject, this))
            {
                _instanceClaims.Add(claim);
            }
            else
            {
                _instanceClaims.Add(claim.Clone(this));
            }
        }
 
        /// <summary>
        /// Retrieves a <see cref="IEnumerable{Claim}"/> where each claim is matched by <paramref name="match"/>.
        /// </summary>
        /// <param name="match">The function that performs the matching logic.</param>
        /// <returns>A <see cref="IEnumerable{Claim}"/> of matched claims.</returns>
        /// <exception cref="ArgumentNullException">if 'match' is null.</exception>
        public virtual IEnumerable<Claim> FindAll(Predicate<Claim> match)
        {
            ArgumentNullException.ThrowIfNull(match);
            return Core(match);
 
            IEnumerable<Claim> Core(Predicate<Claim> match)
            {
                foreach (Claim claim in Claims)
                {
                    if (match(claim))
                    {
                        yield return claim;
                    }
                }
            }
        }
 
        /// <summary>
        /// Retrieves a <see cref="IEnumerable{Claim}"/> where each Claim.Type equals <paramref name="type"/>.
        /// </summary>
        /// <param name="type">The type of the claim to match.</param>
        /// <returns>A <see cref="IEnumerable{Claim}"/> of matched claims.</returns>
        /// <remarks>Comparison is: StringComparison.OrdinalIgnoreCase.</remarks>
        /// <exception cref="ArgumentNullException">if 'type' is null.</exception>
        public virtual IEnumerable<Claim> FindAll(string type)
        {
            ArgumentNullException.ThrowIfNull(type);
            return Core(type);
 
            IEnumerable<Claim> Core(string type)
            {
                foreach (Claim claim in Claims)
                {
                    if (claim != null)
                    {
                        if (string.Equals(claim.Type, type, StringComparison.OrdinalIgnoreCase))
                        {
                            yield return claim;
                        }
                    }
                }
            }
        }
 
        /// <summary>
        /// Retrieves the first <see cref="Claim"/> that is matched by <paramref name="match"/>.
        /// </summary>
        /// <param name="match">The function that performs the matching logic.</param>
        /// <returns>A <see cref="Claim"/>, null if nothing matches.</returns>
        /// <exception cref="ArgumentNullException">if 'match' is null.</exception>
        public virtual Claim? FindFirst(Predicate<Claim> match)
        {
            ArgumentNullException.ThrowIfNull(match);
 
            foreach (Claim claim in Claims)
            {
                if (match(claim))
                {
                    return claim;
                }
            }
 
            return null;
        }
 
        /// <summary>
        /// Retrieves the first <see cref="Claim"/> where Claim.Type equals <paramref name="type"/>.
        /// </summary>
        /// <param name="type">The type of the claim to match.</param>
        /// <returns>A <see cref="Claim"/>, null if nothing matches.</returns>
        /// <remarks>Comparison is: StringComparison.OrdinalIgnoreCase.</remarks>
        /// <exception cref="ArgumentNullException">if 'type' is null.</exception>
        public virtual Claim? FindFirst(string type)
        {
            ArgumentNullException.ThrowIfNull(type);
 
            foreach (Claim claim in Claims)
            {
                if (claim != null)
                {
                    if (string.Equals(claim.Type, type, StringComparison.OrdinalIgnoreCase))
                    {
                        return claim;
                    }
                }
            }
 
            return null;
        }
 
        /// <summary>
        /// Determines if a claim is contained within this ClaimsIdentity.
        /// </summary>
        /// <param name="match">The function that performs the matching logic.</param>
        /// <returns>true if a claim is found, false otherwise.</returns>
        /// <exception cref="ArgumentNullException">if 'match' is null.</exception>
        public virtual bool HasClaim(Predicate<Claim> match)
        {
            ArgumentNullException.ThrowIfNull(match);
 
            foreach (Claim claim in Claims)
            {
                if (match(claim))
                {
                    return true;
                }
            }
 
            return false;
        }
 
        /// <summary>
        /// Determines if a claim with type AND value is contained within this ClaimsIdentity.
        /// </summary>
        /// <param name="type">the type of the claim to match.</param>
        /// <param name="value">the value of the claim to match.</param>
        /// <returns>true if a claim is matched, false otherwise.</returns>
        /// <remarks>Comparison is: StringComparison.OrdinalIgnoreCase for Claim.Type, StringComparison.Ordinal for Claim.Value.</remarks>
        /// <exception cref="ArgumentNullException">if 'type' is null.</exception>
        /// <exception cref="ArgumentNullException">if 'value' is null.</exception>
        public virtual bool HasClaim(string type, string value)
        {
            ArgumentNullException.ThrowIfNull(type);
            ArgumentNullException.ThrowIfNull(value);
 
            foreach (Claim claim in Claims)
            {
                if (claim != null
                        && string.Equals(claim.Type, type, StringComparison.OrdinalIgnoreCase)
                        && string.Equals(claim.Value, value, StringComparison.Ordinal))
                {
                    return true;
                }
            }
 
            return false;
        }
 
        /// <summary>
        /// Initializes from a <see cref="BinaryReader"/>. Normally the reader is initialized with the results from <see cref="WriteTo(BinaryWriter)"/>
        /// Normally the <see cref="BinaryReader"/> is initialized in the same way as the <see cref="BinaryWriter"/> passed to <see cref="WriteTo(BinaryWriter)"/>.
        /// </summary>
        /// <param name="reader">a <see cref="BinaryReader"/> pointing to a <see cref="ClaimsIdentity"/>.</param>
        /// <exception cref="ArgumentNullException">if 'reader' is null.</exception>
        private void Initialize(BinaryReader reader)
        {
            ArgumentNullException.ThrowIfNull(reader);
 
            SerializationMask mask = (SerializationMask)reader.ReadInt32();
            int numPropertiesRead = 0;
            int numPropertiesToRead = reader.ReadInt32();
 
            if ((mask & SerializationMask.AuthenticationType) == SerializationMask.AuthenticationType)
            {
                _authenticationType = reader.ReadString();
                numPropertiesRead++;
            }
 
            if ((mask & SerializationMask.BootstrapConext) == SerializationMask.BootstrapConext)
            {
                _bootstrapContext = reader.ReadString();
                numPropertiesRead++;
            }
 
            if ((mask & SerializationMask.NameClaimType) == SerializationMask.NameClaimType)
            {
                _nameClaimType = reader.ReadString();
                numPropertiesRead++;
            }
            else
            {
                _nameClaimType = ClaimsIdentity.DefaultNameClaimType;
            }
 
            if ((mask & SerializationMask.RoleClaimType) == SerializationMask.RoleClaimType)
            {
                _roleClaimType = reader.ReadString();
                numPropertiesRead++;
            }
            else
            {
                _roleClaimType = ClaimsIdentity.DefaultRoleClaimType;
            }
 
            if ((mask & SerializationMask.HasLabel) == SerializationMask.HasLabel)
            {
                _label = reader.ReadString();
                numPropertiesRead++;
            }
 
            if ((mask & SerializationMask.HasClaims) == SerializationMask.HasClaims)
            {
                int numberOfClaims = reader.ReadInt32();
                for (int index = 0; index < numberOfClaims; index++)
                {
                    _instanceClaims.Add(CreateClaim(reader));
                }
                numPropertiesRead++;
            }
 
            if ((mask & SerializationMask.Actor) == SerializationMask.Actor)
            {
                _actor = new ClaimsIdentity(reader);
                numPropertiesRead++;
            }
 
            if ((mask & SerializationMask.UserData) == SerializationMask.UserData)
            {
                int cb = reader.ReadInt32();
                _userSerializationData = reader.ReadBytes(cb);
                numPropertiesRead++;
            }
 
            for (int i = numPropertiesRead; i < numPropertiesToRead; i++)
            {
                reader.ReadString();
            }
        }
 
        /// <summary>
        /// Provides an extensibility point for derived types to create a custom <see cref="Claim"/>.
        /// </summary>
        /// <param name="reader">the <see cref="BinaryReader"/>that points at the claim.</param>
        /// <returns>a new <see cref="Claim"/>.</returns>
        protected virtual Claim CreateClaim(BinaryReader reader)
        {
            ArgumentNullException.ThrowIfNull(reader);
 
            return new Claim(reader, this);
        }
 
        /// <summary>
        /// Serializes using a <see cref="BinaryWriter"/>
        /// </summary>
        /// <param name="writer">the <see cref="BinaryWriter"/> to use for data storage.</param>
        /// <exception cref="ArgumentNullException">if 'writer' is null.</exception>
        public virtual void WriteTo(BinaryWriter writer)
        {
            WriteTo(writer, null);
        }
 
        /// <summary>
        /// Serializes using a <see cref="BinaryWriter"/>
        /// </summary>
        /// <param name="writer">the <see cref="BinaryWriter"/> to use for data storage.</param>
        /// <param name="userData">additional data provided by derived type.</param>
        /// <exception cref="ArgumentNullException">if 'writer' is null.</exception>
        protected virtual void WriteTo(BinaryWriter writer, byte[]? userData)
        {
            ArgumentNullException.ThrowIfNull(writer);
 
            int numberOfPropertiesWritten = 0;
            var mask = SerializationMask.None;
            if (_authenticationType != null)
            {
                mask |= SerializationMask.AuthenticationType;
                numberOfPropertiesWritten++;
            }
 
            if (_bootstrapContext != null)
            {
                if (_bootstrapContext is string)
                {
                    mask |= SerializationMask.BootstrapConext;
                    numberOfPropertiesWritten++;
                }
            }
 
            if (!string.Equals(_nameClaimType, ClaimsIdentity.DefaultNameClaimType, StringComparison.Ordinal))
            {
                mask |= SerializationMask.NameClaimType;
                numberOfPropertiesWritten++;
            }
 
            if (!string.Equals(_roleClaimType, ClaimsIdentity.DefaultRoleClaimType, StringComparison.Ordinal))
            {
                mask |= SerializationMask.RoleClaimType;
                numberOfPropertiesWritten++;
            }
 
            if (!string.IsNullOrWhiteSpace(_label))
            {
                mask |= SerializationMask.HasLabel;
                numberOfPropertiesWritten++;
            }
 
            if (_instanceClaims.Count > 0)
            {
                mask |= SerializationMask.HasClaims;
                numberOfPropertiesWritten++;
            }
 
            if (_actor != null)
            {
                mask |= SerializationMask.Actor;
                numberOfPropertiesWritten++;
            }
 
            if (userData != null && userData.Length > 0)
            {
                numberOfPropertiesWritten++;
                mask |= SerializationMask.UserData;
            }
 
            writer.Write((int)mask);
            writer.Write(numberOfPropertiesWritten);
            if ((mask & SerializationMask.AuthenticationType) == SerializationMask.AuthenticationType)
            {
                writer.Write(_authenticationType!);
            }
 
            if ((mask & SerializationMask.BootstrapConext) == SerializationMask.BootstrapConext)
            {
                writer.Write((string)_bootstrapContext!);
            }
 
            if ((mask & SerializationMask.NameClaimType) == SerializationMask.NameClaimType)
            {
                writer.Write(_nameClaimType);
            }
 
            if ((mask & SerializationMask.RoleClaimType) == SerializationMask.RoleClaimType)
            {
                writer.Write(_roleClaimType);
            }
 
            if ((mask & SerializationMask.HasLabel) == SerializationMask.HasLabel)
            {
                writer.Write(_label!);
            }
 
            if ((mask & SerializationMask.HasClaims) == SerializationMask.HasClaims)
            {
                writer.Write(_instanceClaims.Count);
                foreach (var claim in _instanceClaims)
                {
                    claim.WriteTo(writer);
                }
            }
 
            if ((mask & SerializationMask.Actor) == SerializationMask.Actor)
            {
                _actor!.WriteTo(writer);
            }
 
            if ((mask & SerializationMask.UserData) == SerializationMask.UserData)
            {
                writer.Write(userData!.Length);
                writer.Write(userData);
            }
 
            writer.Flush();
        }
 
        /// <summary>
        /// Checks if a circular reference exists to 'this'
        /// </summary>
        /// <param name="subject"></param>
        /// <returns></returns>
        private bool IsCircular(ClaimsIdentity subject)
        {
            if (ReferenceEquals(this, subject))
            {
                return true;
            }
 
            ClaimsIdentity currSubject = subject;
 
            while (currSubject.Actor != null)
            {
                if (ReferenceEquals(this, currSubject.Actor))
                {
                    return true;
                }
 
                currSubject = currSubject.Actor;
            }
 
            return false;
        }
 
        /// <summary>
        /// Populates the specified <see cref="SerializationInfo"/> with the serialization data for the ClaimsIdentity
        /// </summary>
        /// <param name="info">The serialization information stream to write to. Satisfies ISerializable contract.</param>
        /// <param name="context">Context for serialization. Can be null.</param>
        /// <exception cref="ArgumentNullException">Thrown if the info parameter is null.</exception>
        protected virtual void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            throw new PlatformNotSupportedException();
        }
 
        internal string DebuggerToString()
        {
            // DebuggerDisplayAttribute is inherited. Use virtual members instead of private fields to gather data.
            int claimsCount = 0;
            foreach (Claim item in Claims)
            {
                claimsCount++;
            }
 
            string debugText = $"IsAuthenticated = {(IsAuthenticated ? "true" : "false")}";
            if (Name != null)
            {
                // The ClaimsIdentity.Name property requires that ClaimsIdentity.NameClaimType is correctly
                // configured to match the name of the logical name claim type of the identity.
                // Because of this, only include name if the ClaimsIdentity.Name property has a value.
                // Not including the name is to avoid developer confusion at seeing "Name = (null)" on an authenticated identity.
                debugText += $", Name = {Name}";
            }
            if (claimsCount > 0)
            {
                debugText += $", Claims = {claimsCount}";
            }
 
            return debugText;
        }
    }
}