WCF: Discover In WCF World
What is service identity in WCF WSDL ?
For normal basicHttpBinding we use one directional authentication where only service authenticates the client. For dual authentication, we allow client to authenticate the service as well.
With WS Standard we use the Mutual authentication scheme and publish the service identity to the clients.
The service identity becomes part of WSDL and is available to clients, which gets used by client to authenticate to service. Client compares the endpoint identity value (local) with the actual value the endpoint authentication process returned. If they match, the client is assured it has contacted the expected service endpoint. This protects client to talk to phishing service.
When the client initiates a secure channel to send a message to a service over it, the Windows Communication Foundation (WCF) infrastructure authenticates the service, and only sends the message if the service identity matches the identity specified in the endpoint address the client uses.
Identity processing consists of the following stages:
• At design time, the client developer determines the service's identity from the endpoint's metadata (exposed through WSDL).
• At runtime, the client application checks the claims of the service's security credentials before sending any messages to the service.
Client does not send messages to the service until the service's credentials have been authenticated based on what is known in advance from the service's metadata.
UPN IDENTITY:
This ensures that the service is running under a specific Windows user account.
The user account can be either the current logged-on user or the service running under a particular user account.
This setting takes advantage of Windows Kerberos security if the service is running under a domain account within an Active Directory environment.
SPN IDENTITY:
This ensures that the SPN and the specific Windows account associated with the SPN identify the service.
You can use the Setspn.exe tool to associate a machine account for the service's user account.
This setting takes advantage of Windows Kerberos security if the service is running under one of the system accounts or under a domain account that has an associated SPN name with it and the computer is a member of a domain within an Active Directory environment.
If the service authenticates using message- or transport-level SSL with a Windows credential for authentication, and negotiates the credential, the following identity values are valid:
• DNS. The negotiation passes the service's SPN so that the DNS name can be checked. The SPN is in the form host/<dns name>.
• SPN. An explicit service SPN is returned, for example, host/myservice.
• UPN. The UPN of the service account. The UPN is in the form username@domain. For example, when the service is running in a user account, it may be username@contoso.com.
If no identity is specified, and the client credential type is Windows, the default is SPN with the value set to the hostname part of the service endpoint address prefixed with the "host/" literal.
For Windows Authentication
SPN or UPN are published in WSDL because ....
Kerberos authentication requires that a UPN or SPN be supplied to the client to authenticate the service.
From IIS 7 onwards, we introduced a new concept called – KERNEL MODE SECURITY.
This is intended to simplify the SPN management and move authentication to Kernel layer.
With this security mode, the SPN is registered under the Machine Account.
If our web application pool is hosted in a custom domain account, the SPN must be registered for that user account, rather than the machine account.
https://technet.microsoft.com/en-us/library/dd632778.aspx
Stack used for checking the identity at client side:
at System.ServiceModel.Security.IdentityVerifier.DefaultIdentityVerifier.TryGetIdentity(EndpointAddress reference, EndpointIdentity& identity)
at System.ServiceModel.Channels.WindowsStreamSecurityUpgradeProvider.WindowsStreamSecurityUpgradeInitiator.InitiateUpgradePrepare(Stream stream, NegotiateStream& negotiateStream, String& targetName, EndpointIdentity& identity)
Source code method:
======================
public override bool TryGetIdentity(EndpointAddress reference, out EndpointIdentity identity)
{
if (reference == null)
{
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("reference");
}
identity = reference.Identity;
if (identity == null)
{
identity = this.TryCreateDnsIdentity(reference);
}
if (identity == null)
{
SecurityTraceRecordHelper.TraceIdentityDeterminationFailure(reference, typeof(IdentityVerifier.DefaultIdentityVerifier));
return false;
}
SecurityTraceRecordHelper.TraceIdentityDeterminationSuccess(reference, identity, typeof(IdentityVerifier.DefaultIdentityVerifier));
return true;
}
SCENERIO 1:
=====================
Client passing the correct identity as UPN ...
Stack:
at System.ServiceModel.Security.IdentityVerifier.DefaultIdentityVerifier.TryGetIdentity(EndpointAddress reference, EndpointIdentity& identity)
EndPointAddress object is created based on the identity set in the config file.
We return success from the highlighted code…
public override bool TryGetIdentity(EndpointAddress reference, out EndpointIdentity identity)
{
if (reference == null)
{
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("reference");
}
identity = reference.Identity;
if (identity == null)
{
identity = this.TryCreateDnsIdentity(reference);
}
if (identity == null)
{
SecurityTraceRecordHelper.TraceIdentityDeterminationFailure(reference, typeof(IdentityVerifier.DefaultIdentityVerifier));
return false;
}
SecurityTraceRecordHelper.TraceIdentityDeterminationSuccess(reference, identity, typeof(IdentityVerifier.DefaultIdentityVerifier));
return true;
}
<DataItem>
<TraceRecord xmlns="https://schemas.microsoft.com/2004/10/E2ETraceEvent/TraceRecord" Severity="Information">
<TraceIdentifier>https://msdn.microsoft.com/en-US/library/System.ServiceModel.Security.SecurityIdentityDeterminationSuccess.aspx</TraceIdentifier>
<Description>Identity was determined for an EndpointReference.</Description>
<AppDomain>ConsoleApplication1.vshost.exe</AppDomain>
<ExtendedData xmlns="https://schemas.microsoft.com/2006/08/ServiceModel/ServiceIdentityDeterminationTraceRecord">
<IdentityVerifierType>System.ServiceModel.Security.IdentityVerifier+DefaultIdentityVerifier</IdentityVerifierType>
<Identity xmlns="https://schemas.xmlsoap.org/ws/2006/02/addressingidentity">
<Upn>saurabs@fareast.corp.microsoft.com</Upn>
</Identity>
<EndpointReference xmlns="https://www.w3.org/2005/08/addressing">
<Address>net.tcp://saurabh21.fareast.corp.microsoft.com/TCP/Service1.svc</Address>
<Identity xmlns="https://schemas.xmlsoap.org/ws/2006/02/addressingidentity">
<Upn>saurabs@fareast.corp.microsoft.com</Upn>
</Identity>
</EndpointReference>
</ExtendedData>
</TraceRecord>
</DataItem>
Next we try to get the Associated SPN for the identity …
internal static string GetSpnFromIdentity(EndpointIdentity identity, EndpointAddress target)
{
bool foundSpn = false;
string spn = null;
if (identity != null)
{
if (ClaimTypes.Spn.Equals(identity.IdentityClaim.ClaimType))
{
spn = (string)identity.IdentityClaim.Resource;
foundSpn = true;
}
else if (ClaimTypes.Upn.Equals(identity.IdentityClaim.ClaimType))
{
spn = (string)identity.IdentityClaim.Resource;
foundSpn = true;
}
else if (ClaimTypes.Dns.Equals(identity.IdentityClaim.ClaimType))
{
spn = String.Format(CultureInfo.InvariantCulture, "host/{0}", (string)identity.IdentityClaim.Resource);
foundSpn = true;
}
}
if (!foundSpn)
{
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new MessageSecurityException(SR.GetString(SR.CannotDetermineSPNBasedOnAddress, target)));
}
return spn;
}
Next we see..
at System.ServiceModel.Diagnostics.SecurityTraceRecordHelper.TraceIdentityVerificationSuccess(EventTraceActivity eventTraceActivity, EndpointIdentity identity, Claim claim, Type identityVerifier)
at System.ServiceModel.Security.IdentityVerifier.DefaultIdentityVerifier.CheckAccess(EndpointIdentity identity, AuthorizationContext authContext)
We are calling the CheckAccess method to confirm the claims received as a part of service identity...
Confirm if the claims are for same identity what we specified..
public override bool CheckAccess(EndpointIdentity identity, AuthorizationContext authContext)
{
if (identity == null)
{
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("identity");
}
if (authContext == null)
{
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("authContext");
}
for (int i = 0; i < authContext.ClaimSets.Count; i++)
{
ClaimSet claimSet = authContext.ClaimSets[i];
if (claimSet.ContainsClaim(identity.IdentityClaim))
{
SecurityTraceRecordHelper.TraceIdentityVerificationSuccess(identity, identity.IdentityClaim, base.GetType());
return true;
}
string expectedSpn = null;
if (ClaimTypes.Dns.Equals(identity.IdentityClaim.ClaimType))
{
expectedSpn = string.Format(CultureInfo.InvariantCulture, "host/{0}", new object[] { (string) identity.IdentityClaim.Resource });
Claim claim = this.CheckDnsEquivalence(claimSet, expectedSpn);
if (claim != null)
{
SecurityTraceRecordHelper.TraceIdentityVerificationSuccess(identity, claim, base.GetType());
return true;
}
}
SecurityIdentifier identitySid = null;
if (ClaimTypes.Sid.Equals(identity.IdentityClaim.ClaimType))
{
identitySid = this.GetSecurityIdentifier(identity.IdentityClaim);
}
else if (ClaimTypes.Upn.Equals(identity.IdentityClaim.ClaimType))
{
identitySid = ((UpnEndpointIdentity) identity).GetUpnSid();
}
else if (ClaimTypes.Spn.Equals(identity.IdentityClaim.ClaimType))
{
identitySid = ((SpnEndpointIdentity) identity).GetSpnSid();
}
else if (ClaimTypes.Dns.Equals(identity.IdentityClaim.ClaimType))
{
identitySid = new SpnEndpointIdentity(expectedSpn).GetSpnSid();
}
if (identitySid != null)
{
Claim claim2 = this.CheckSidEquivalence(identitySid, claimSet);
if (claim2 != null)
{
SecurityTraceRecordHelper.TraceIdentityVerificationSuccess(identity, claim2, base.GetType());
return true;
}
}
}
SecurityTraceRecordHelper.TraceIdentityVerificationFailure(identity, authContext, base.GetType());
return false;
}
Dumping objects:
Our identity Object:
0:000> !do 0x27d97ec
Name: System.ServiceModel.UpnEndpointIdentity
MethodTable: 51800748
EEClass: 51444ad8
Size: 32(0x20) bytes
File: C:\windows\Microsoft.Net\assembly\GAC_MSIL\System.ServiceModel\v4.0_4.0.0.0__b77a5c561934e089\System.ServiceModel.dll
Fields:
MT Field Offset Type VT Attr Value Name
4ffcb198 4002575 4 ...odel.Claims.Claim 0 instance 027d9930 identityClaim
4ffcb2b8 4002576 8 ...m.IdentityModel]] 0 instance 00000000 claimComparer
65dc15b8 400257d c ...ecurityIdentifier 0 instance 00000000 upnSid
65dc6f18 400257e 18 System.Boolean 1 instance 0 hasUpnSidBeenComputed
65dc29b4 400257f 10 ...l.WindowsIdentity 0 instance 00000000 windowsIdentity
65dcb060 4002580 14 System.Object 0 instance 027d980c thisLock
0:000> !DumpObj /d 027d9930
Name: System.IdentityModel.Claims.Claim
MethodTable: 4ffcb198
EEClass: 4ff64b38
Size: 24(0x18) bytes
File: C:\windows\Microsoft.Net\assembly\GAC_MSIL\System.IdentityModel\v4.0_4.0.0.0__b77a5c561934e089\System.IdentityModel.dll
Fields:
MT Field Offset Type VT Attr Value Name
65dcacc0 40007a9 4 System.String 0 instance 027d98b0 claimType
65dcb060 40007aa 8 System.Object 0 instance 02732d6c resource
65dcacc0 40007ab c System.String 0 instance 027d9818 right
4ffcb2b8 40007ac 10 ...m.IdentityModel]] 0 instance 027d9954 comparer
4ffcb198 40007a8 dc ...odel.Claims.Claim 0 static 0293d9b8 system
0:000> !DumpObj /d 027d98b0
Name: System.String
MethodTable: 65dcacc0
EEClass: 659d486c
Size: 128(0x80) bytes
File: C:\windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String: https://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn
Fields:
MT Field Offset Type VT Attr Value Name
65dcc480 40000aa 4 System.Int32 1 instance 57 m_stringLength
65dcb6b8 40000ab 8 System.Char 1 instance 68 m_firstChar
65dcacc0 40000ac c System.String 0 shared static Empty
>> Domain:Value 008867e0:NotInit <<
0:000> !DumpObj /d 02732d6c
Name: System.String
MethodTable: 65dcacc0
EEClass: 659d486c
Size: 82(0x52) bytes
File: C:\windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String: saurabs@fareast.corp.microsoft.com
Fields:
MT Field Offset Type VT Attr Value Name
65dcc480 40000aa 4 System.Int32 1 instance 34 m_stringLength
65dcb6b8 40000ab 8 System.Char 1 instance 73 m_firstChar
65dcacc0 40000ac c System.String 0 shared static Empty
Authorization context received from the service…
0:000> !do 0x293db44
Name: System.IdentityModel.SecurityUtils+SimpleAuthorizationContext
MethodTable: 4ffcbcb0
EEClass: 4ff6c610
Size: 20(0x14) bytes
File: C:\windows\Microsoft.Net\assembly\GAC_MSIL\System.IdentityModel\v4.0_4.0.0.0__b77a5c561934e089\System.IdentityModel.dll
Fields:
MT Field Offset Type VT Attr Value Name
4ffcb998 40011db 4 ....SecurityUniqueId 0 instance 00000000 id
4ffcbb90 40011dc 8 ...conditionalPolicy 0 instance 0293d8bc policy
659d86a4 40011dd c ...bject, mscorlib]] 0 instance 0293db7c properties
0:000> !DumpObj /d 0293d8bc
Name: System.IdentityModel.Policy.UnconditionalPolicy
MethodTable: 4ffcbb90
EEClass: 4ff6c574
Size: 40(0x28) bytes
File: C:\windows\Microsoft.Net\assembly\GAC_MSIL\System.IdentityModel\v4.0_4.0.0.0__b77a5c561934e089\System.IdentityModel.dll
Fields:
MT Field Offset Type VT Attr Value Name
4ffcb998 400088d 4 ....SecurityUniqueId 0 instance 00000000 id
4ffcbbec 400088e 8 ...l.Claims.ClaimSet 0 instance 0293d9e8 issuer
4ffcbbec 400088f c ...l.Claims.ClaimSet 0 instance 0293d88c issuance
4ff5179c 4000890 10 ...m.IdentityModel]] 0 instance 00000000 issuances
65dc8bdc 4000891 1c System.DateTime 1 instance 0293d8d8 expirationTime
65dc3968 4000892 14 ...incipal.IIdentity 0 instance 0293d7b8 primaryIdentity
65dc6f18 4000893 18 System.Boolean 1 instance 0 disposable
65dc6f18 4000894 19 System.Boolean 1 instance 0 disposed
0:000> !DumpObj /d 0293d7b8
Name: System.Security.Principal.GenericIdentity
MethodTable: 65dc2d48
EEClass: 65adcafc
Size: 64(0x40) bytes
File: C:\windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
Fields:
MT Field Offset Type VT Attr Value Name
659e0bf0 4001b15 4 ...Claim, mscorlib]] 0 instance 0293d7f8 m_instanceClaims
659e0c90 4001b16 8 ...lib]], mscorlib]] 0 instance 0293d810 m_externalClaims
65dcacc0 4001b17 c System.String 0 instance 0293d43c m_nameType
65dcacc0 4001b18 10 System.String 0 instance 0293d548 m_roleType
65dcacc0 4001b19 14 System.String 0 instance 0293d428 m_version
65dc652c 4001b1a 18 ...ms.ClaimsIdentity 0 instance 00000000 m_actor
65dcacc0 4001b1b 1c System.String 0 instance 00000000 m_authenticationType
65dcb060 4001b1c 20 System.Object 0 instance 00000000 m_bootstrapContext
65dcacc0 4001b1d 24 System.String 0 instance 00000000 m_label
65dcacc0 4001b1e 28 System.String 0 instance 00000000 m_serializedNameType
65dcacc0 4001b1f 2c System.String 0 instance 00000000 m_serializedRoleType
65dcacc0 4001b20 30 System.String 0 instance 00000000 m_serializedClaims
65dcacc0 4001b2a 34 System.String 0 instance 02732d6c m_name
65dcacc0 4001b2b 38 System.String 0 instance 02621228 m_type
0:000> !DumpObj /d 02732d6c
Name: System.String
MethodTable: 65dcacc0
EEClass: 659d486c
Size: 82(0x52) bytes
File: C:\windows\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
String: saurabs@fareast.corp.microsoft.com
Fields:
MT Field Offset Type VT Attr Value Name
65dcc480 40000aa 4 System.Int32 1 instance 34 m_stringLength
65dcb6b8 40000ab 8 System.Char 1 instance 73 m_firstChar
65dcacc0 40000ac c System.String 0 shared static Empty
>> Domain:Value 008867e0:NotInit <<
We can see the identity in both is same..
And eventually the identity verification succeeds...
Note:
The method is only called if we are on Kerberos authentication scheme..
Otherwise we fall back to NTLM..
Next case demonstrate the same…
Scenario 2:
================
When client don't specify any identity ......
However he is supposed to specify as UPN.....
Stack:
at System.ServiceModel.Diagnostics.SecurityTraceRecordHelper.TraceIdentityDeterminationSuccess(EndpointAddress epr, EndpointIdentity identity, Type identityVerifier)
at System.ServiceModel.Security.IdentityVerifier.DefaultIdentityVerifier.TryGetIdentity(EndpointAddress reference, EndpointIdentity& identity)
<TraceRecord xmlns="https://schemas.microsoft.com/2004/10/E2ETraceEvent/TraceRecord" Severity="Information">
<TraceIdentifier>https://msdn.microsoft.com/en-US/library/System.ServiceModel.Security.SecurityIdentityDeterminationSuccess.aspx</TraceIdentifier>
<Description>Identity was determined for an EndpointReference.</Description>
<AppDomain>ConsoleApplication1.vshost.exe</AppDomain>
<ExtendedData xmlns="https://schemas.microsoft.com/2006/08/ServiceModel/ServiceIdentityDeterminationTraceRecord">
<IdentityVerifierType>System.ServiceModel.Security.IdentityVerifier+DefaultIdentityVerifier</IdentityVerifierType>
<Identity xmlns="https://schemas.xmlsoap.org/ws/2006/02/addressingidentity">
<Dns>saurabh21.fareast.corp.microsoft.com</Dns>
</Identity>
<EndpointReference xmlns="https://www.w3.org/2005/08/addressing">
<Address>net.tcp://saurabh21.fareast.corp.microsoft.com/TCP/Service1.svc</Address>
</EndpointReference>
</ExtendedData>
</TraceRecord>
We can see here we end up in setting the DNS identity....
Because of this code....
identity = reference.Identity;
if (identity == null)
{
identity = this.TryCreateDnsIdentity(reference);
}
private EndpointIdentity TryCreateDnsIdentity(EndpointAddress reference)
{
Uri uri = reference.Uri;
if (!uri.IsAbsoluteUri)
{
return null;
}
return EndpointIdentity.CreateDnsIdentity(uri.DnsSafeHost);
}
So it reads the service URL and get the DNSSafeHost value from there...
In my case...
URI is: net.tcp://saurabh21.fareast.corp.microsoft.com/TCP/Service1.svc
so "DnsSafeHost" is "saurabh21.fareast.corp.microsoft.com"
Since my service also happen to run on same box so we are good to go in this case...
BUT … this is good to go only if we ok with NTLM …
Since we did not specified the correct UPN, we have fallen back to NTLM…
If I set an end point behavior at client side, where I set allowNTLM = false..
Mutual authentication check will fail…
Then the Case 2 will fail…. As expected….
Error:
The remote server did not satisfy the mutual authentication requirement.
Source Code…
void ValidateMutualAuth(EndpointIdentity expectedIdentity, NegotiateStream negotiateStream,
SecurityMessageProperty remoteSecurity, bool allowNtlm)
{
if (negotiateStream.IsMutuallyAuthenticated)
{
if (expectedIdentity != null)
{
if (!parent.IdentityVerifier.CheckAccess(expectedIdentity,
remoteSecurity.ServiceSecurityContext.AuthorizationContext))
{
string primaryIdentity = SecurityUtils.GetIdentityNamesFromContext(remoteSecurity.ServiceSecurityContext.AuthorizationContext);
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityNegotiationException(SR.GetString(
SR.RemoteIdentityFailedVerification, primaryIdentity)));
}
}
}
else if (!allowNtlm)
{
throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new SecurityNegotiationException(SR.GetString(
SR.StreamMutualAuthNotSatisfied)));
}
}
EVEN NTLM check may fail… if the host header and the DNS name are different…
if I host my service runs on a different box name... not on saurabh21.fareast.corp.microsoft.com
Then the check will eventually fail... and this will eventually help us detect the phishing sites…
Let’s see how...
I configured my WCF service to run on XYZ.com as host header.....
So the end point to client now becomes....
net.tcp://xyz.com/TCP/Service1.svc
Eventually the DNS value computed by client (in case we don't add any UPN) will be XYZ.com
In this case, when the client receives the Windows (Kerberos) credentials /claims for the service, it expects to see the DNS value to be XYZ.com.
and we know this is invalid.. so finally server reject the negotiation with this error...
SCENERIO 3
==============
If we pass blank or wrong UPN ... why it works ?
It works because we fall back to NTLM...
When falling back to NTLM, no matter what we specify it works.... because we are no longer on the Kerberos layer.
To stop this behavior..
we can set the allowNTLM = false in the client end point behavior...
Once done we no longer be able to get the blank or wrong UPN working...
<behaviors>
<endpointBehaviors>
<behavior name="my">
<clientCredentials>
<windows allowNtlm="false"/>
</clientCredentials>
</behavior>
</endpointBehaviors>
</behaviors>
<client>
<endpoint address="net.tcp://xyz.com/TCP/Service1.svc" binding="netTcpBinding" behaviorConfiguration="my"
bindingConfiguration="net" contract="ServiceReference1.IService1"
name="net">
<identity>
<userPrincipalName value="" />
</identity>
</endpoint>
</client>
SPNEGO and KERBEROS
https://thekspace.com/home/component/content/article/54-kerberos-and-spnego.html
Important link:
https://msdn.microsoft.com/en-us/library/bb628618.aspx
Net.Tcp internally relies on the SPNEGO protocol ... and we know for sure that falling back to NTLM will work
Until we explicitly set it to false in end point behavior...
==================================================
https://msdn.microsoft.com/en-us/library/dd357379.aspx
https://www.ietf.org/rfc/rfc4178.txt
Hope the content help in understanding the <identity> in WCF World.
Comments
- Anonymous
January 05, 2015
Wow, thank you! I had to figure out Scenario #2 and Scenario #3 the hard way. It's very validating to read something like this directly from Microsoft. I speak from a place of ignorance on my part, but I think that its just plain silly to require clients to specify the SPN in advance. If the WCF service is being ran under the Network Service account (i.e. DefaultApplicationPool identity in IIS) and no SPN is specified on the client, the client will assume the default of HOST/<hostname>; however, if you run IIS as a domain account user you have to do so much more:
- Set the SPN for DomainUser to something (i.e. Service/<hostname> or Company/Service or something).
- Then you have to update the client to handle the SPN... but wait! Now the client will not understand HOST/<hostname>! Why can't clients just figure this out on their own. Now developers have to dig and dig and hopefully come up with something that won't break things or violate some security measure.