WCF over net.tcp with token authentication, loses Claims between TokenHandler and AuthorisationManager

StackOverflow https://stackoverflow.com/questions/17726480

Question

we using a custom binding in WCF to authenticate using a security token (SAML). We are finding that we are getting server-side and seeing the TokenHandler (derived from Saml11SecurityTokenHandler) correctly process and authorise the token then return a new ClaimsIdentity.

However, when the processing then calls on to the AuthorisationManager.CheckAccessCore (derived from IdentityModelServiceAuthorizationManager), the operationContext.ServiceSecurityContext.PrimaryIdentity is a GenericIdentity with nothing populated.

WE have a http implementation of the binding below which is very similar, and workd fine, we can see the token being validated and the ClaimsIdentity being returned, then we observe the AuthorisationManager processing the same identity and allowing them through.

The netTcp binding is a code based one and looks like this:

    /// <summary>
    /// NetTcp binding that supports a Saml token being passed
    /// </summary>
    public class SamlNetTcpBinding : CustomBinding
    {
        private readonly TcpTransportBindingElement _transportBindingElement;
        private readonly BinaryMessageEncodingBindingElement _encodingBindingElement;
        // private readonly SecurityBindingElement _securityBindingElement;

        /// <summary>
        /// Initializes a new instance of the <see cref="SamlNetTcpBinding"/> class.
        /// </summary>
        public SamlNetTcpBinding()
        {
            IssuerAddress = "http://www.myIssuerAddress.com/";

            _transportBindingElement = new TcpTransportBindingElement()
            {
                TransferMode = TransferMode.Streamed, PortSharingEnabled = true
            }; 
            _encodingBindingElement = new BinaryMessageEncodingBindingElement();   
        }

        /// <summary>
        /// Returns a generic collection of the binding elements from the custom binding.
        /// </summary>
        /// <returns>
        /// An <see cref="T:System.Collections.Generic.ICollection`1" /> object of type <see cref="T:System.ServiceModel.Channels.BindingElement" /> that contains the binding elements from the custom binding.
        /// </returns>
        public override BindingElementCollection CreateBindingElements()
        {
            return new BindingElementCollection()
            {
                new TransactionFlowBindingElement(TransactionProtocol.WSAtomicTransactionOctober2004),
                CreateSecurityBindingElement(),
                new SslStreamSecurityBindingElement(),
                _encodingBindingElement,
                _transportBindingElement
            };
        }

        /// <summary>
        /// Provide definition for the scheme.
        /// </summary>
        /// <returns>The URI scheme for transport used by the custom binding; or an empty string if there is no transport (<see cref="T:System.ServiceModel.Channels.TransportBindingElement" /> is null).</returns>
        public override String Scheme
        {
            get { return "net.tcp"; }
        }

        /// <summary>
        /// Gets or sets the issuer address.
        /// </summary>
        /// <value>
        /// The issuer address.
        /// </value>
        public string IssuerAddress { get; set; }

        /// <summary>
        /// Create client side binding certificate.
        /// </summary>
        /// <returns>A security Binding element</returns>
        private SecurityBindingElement CreateSecurityBindingElement()
        {
            var protectionParameters = new X509SecurityTokenParameters(
                X509KeyIdentifierClauseType.Thumbprint, SecurityTokenInclusionMode.AlwaysToRecipient);

            // Configure token issuance parameters.
            var parameters = new IssuedSecurityTokenParameters(
                SecurityTokenTypes.OasisWssSaml11TokenProfile11,
                new EndpointAddress(IssuerAddress),
                new BasicHttpBinding())
            {
                KeyType = System.IdentityModel.Tokens.SecurityKeyType.BearerKey,
                InclusionMode = SecurityTokenInclusionMode.AlwaysToRecipient
            };

            var element = SecurityBindingElement.CreateIssuedTokenOverTransportBindingElement(parameters);
            element.MessageSecurityVersion = MessageSecurityVersion.WSSecurity11WSTrust13WSSecureConversation13WSSecurityPolicy12BasicSecurityProfile10;
            element.EndpointSupportingTokenParameters.Endorsing.Add(protectionParameters);

            return element;
        }
    }

Any ideas or suggestions is greatly appreciated. Due to the .net plumbing handling a lot of the orchestration - it's difficult to figure where the identity is being lost. I'm fairly confident that the System.ServiceModel is losing it somewhere, what's unlcear is why net.tcp transport causes this and http doesn't.

THanks

Was it helpful?

Solution

(for the info of readers, I know Chubby Arse and we have discussed this offline including sharing some additional code but I'm posting what I can here to help anyone else who may hit the same problem)

ServiceSecurityContext.PrimaryIdentity will only return the ClaimsIdentity if it is the only one in scope. If there are more than 1 identities present, then it cannot identify which is the primary and so a generic identity is returned.

In your scenario, you have 2 identities in context: your claims identity from the SAML token and also one representing the client certificate that was attached by the caller, something required for net.tcp but not for basicHttp for authentication purposes. In order to access the ClaimsIdentity you need to update your ClaimsServiceAuthorisationManager as follows:

        var identity = securityContext.PrimaryIdentity as IClaimsIdentity;
        if (identity == null)
        {
            // If there is more than 1 identity, for example if there is also a certificate then PrimaryIdentity will be null.
            if (securityContext.AuthorizationContext.Properties.ContainsKey("Principal"))
            {
                var principal = securityContext.AuthorizationContext.Properties["Principal"] as IClaimsPrincipal;
                if (principal != null)
                {
                    identity = principal.Identity as IClaimsIdentity;
                }
            }

            if (identity == null)
            {
                throw new InvalidOperationException("PrimaryIdentity identity is not an IClaimsIdentity");
            }
        }
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top