Ever wondered if and how identities are carried from service to service?
Unable to make much progress on VSTS due to the build bug we are investigating, we recently had a discussion around the identity topic. At the time I believed that writing a quick prototype may not be such a bad thing and here is the brief story for those interested. Note that we are trying to show the flow of identities, without getting embroiled in Kerberos, Tickets and other security subsystem features which are documented in detail on MSDN. If you are looking to secure your service we encourage you to have a look at the Web Service Extensions 3 (WSE3) or Windows Communication Foundation (WCF), formely known as Indigo.
Prerequisites
I created two local account, one called Research and the other Impersonator.
Context

In a nutshell I wanted to investigate the following simple steps:
-
WinForms Application attached the credentials to the proxy (P1) and makes a call to Web Service 1.
-
Web Service 1 runs as a trusted account, i.e. does not impersonate calling user.
-
Web Service 1 displays "running as user" and "calling user is".
-
Web Service 1 then impersonates calling user and makes a call to Web Service 2. Should I simply impersonate, or impersonate and then set the credentials on proxy 2 (p2)?
-
Web Service 2 displays "running as user" and "calling user is" ... which should be one and the same.
Web Service 2
Config
The web service was created using VS.NET 2005 and then registered in IIS … virtual directory, setting security as follows … note that anonymous access is turned off.

Additionally the web.config file was updated as follows:
<identity impersonate="true" />
Source
[WebMethod ( EnableSession = false, TransactionOption = System.EnterpriseServices.TransactionOption.Disabled,
Description = "Prototype method which returns the identity of the host and calling thread, optionally calling another webservice with similar functionality" )]
public string RetrieveSecurityInformation ()
{
StringBuilder returnInformation = new StringBuilder ();
WindowsIdentity identityCurrent = WindowsIdentity.GetCurrent ();
IIdentity identityCaller = Thread.CurrentPrincipal.Identity;
// 1. Build Current Identity Information
returnInformation.Append ( "SERVICE B: CURRENT IDENTTITY INFORMATION --> [" );
returnInformation.Append ( " User Name: " + identityCurrent.Name );
returnInformation.Append ( " Is Anonynmous: " + identityCurrent.IsAnonymous );
returnInformation.Append ( " Is Athenticated: " +
identityCurrent.IsAuthenticated );
returnInformation.Append ( " Is Guest: " + identityCurrent.IsGuest );
returnInformation.Append ( " Is System: " + identityCurrent.IsSystem );
returnInformation.Append ( " Authentication Type: " +
identityCurrent.AuthenticationType );
returnInformation.Append ( " ] " );
returnInformation.Append ( Environment.NewLine );
// 2. Build Caller Identity Information
returnInformation.Append ( "SERVICE B: CALLER IDENTTITY INFORMATION --> [" );
returnInformation.Append ( " User Name: " + identityCaller.Name );
returnInformation.Append ( " Is Athenticated: " +
identityCaller.IsAuthenticated );
returnInformation.Append ( " Authentication Type: "
identityCaller.AuthenticationType );
returnInformation.Append ( " ] " );
returnInformation.Append ( Environment.NewLine );
// Return the information€
return (returnInformation.ToString ());
}
Web Service 1
Config
The web service was created using VS.NET 2005 and then registered in IIS … virtual directory, setting security as follows … note that anonymous access is turned on:

Additionally the web.config file was updated as follows:
<identity impersonate="true" userName="willyxpacer\research" password="nicetry" />
Source
[WebMethod ( EnableSession = false,
TransactionOption = System.EnterpriseServices.TransactionOption.Disabled,
Description = "Prototype method which returns the identity of the host and
calling thread, optionally calling another webservice with
similar functionality" )]
public string RetrieveSecurityInformation ( bool makeRemoteCall,
bool impersonateCaller,
bool setProxyCredentials,
bool setOtherUser,
string url )
{
StringBuilder returnInformation = new StringBuilder ();
WindowsIdentity identityCurrent = WindowsIdentity.GetCurrent ();
WindowsIdentity identityCaller = (WindowsIdentity)
Thread.CurrentPrincipal.Identity;
// 1. Build Current Identity Information
returnInformation.Append ( "SERVICE A: CURRENT IDENTTITY INFORMATION --> [" );
returnInformation.Append ( " User Name: " + identityCurrent.Name );
returnInformation.Append ( " Is Anonynmous: " + identityCurrent.IsAnonymous );
returnInformation.Append ( " Is Athenticated:" +
identityCurrent.IsAuthenticated );
returnInformation.Append ( " Is Guest: " + identityCurrent.IsGuest );
returnInformation.Append ( " Is System: " + identityCurrent.IsSystem );
returnInformation.Append ( " Authentication Type: " +
identityCurrent.AuthenticationType );
returnInformation.Append ( " ] ");
returnInformation.Append ( Environment.NewLine );
// 2. Build Caller Identity Information
returnInformation.Append ( "SERVICE A: CALLER IDENTTITY INFORMATION --> [" );
returnInformation.Append ( " User Name: " + identityCaller.Name );
returnInformation.Append ( " Is Athenticated: " +
identityCaller.IsAuthenticated );
returnInformation.Append ( " Authentication Type: " +
identityCaller.AuthenticationType );
returnInformation.Append ( " ] ");
returnInformation.Append ( Environment.NewLine );
if ( makeRemoteCall )
{
// 3. Call Remote Service
ServiceB.ServiceB remoteService = new ServiceB.ServiceB();
if ( ( null != url ) && ( 0 != url.Length ) )
{
remoteService.Url = url;
}
else
{
remoteService.Url = http://localhost/ServiceB/Service.asmx;
}
if ( impersonateCaller )
{
To impersonate we need to create a WindowsImpersonationContext object, assign the identity and call Impersonate().
// Impersonate
WindowsImpersonationContext impersonation = ((WindowsIdentity
(identityCaller)).Impersonate ();
WindowsIdentity identityTemp = WindowsIdentity.GetCurrent ();
returnInformation.Append ( "SERVICE A: IMPERSONATED IDENTTITY INFORMATION --> [" );
returnInformation.Append ( " User Name: " + identityTemp.Name );
returnInformation.Append ( " Is Anonynmous: " +
identityTemp.IsAnonymous );
returnInformation.Append ( " Is Athenticated: " +
identityTemp.IsAuthenticated );
returnInformation.Append ( " Is Guest: " + identityTemp.IsGuest );
returnInformation.Append ( " Is System: " + identityTemp.IsSystem );
returnInformation.Append ( " Authentication Type: " +
identityTemp.AuthenticationType );
returnInformation.Append ( " ] " );
returnInformation.Append ( Environment.NewLine );
if ( setProxyCredentials )
{
remoteService.PreAuthenticate = true;
remoteService.Credentials = CredentialCache.DefaultCredentials;
}
returnInformation.Append ( remoteService.RetrieveSecurityInformation () );
We must Undo the impersonation … this is probably a good place to use the finally{}.
// Revert
impersonation.Undo();
}
else
{
The “setOtherUser” code simply demonstrats how we can create a network credential object and assign them to the service proxy.
if ( setOtherUser )
{
NetworkCredential credential = new NetworkCredential ( "impersonator",
"password",
"willyxpacer" );
remoteService.PreAuthenticate = true;
remoteService.Credentials = credential;
}
else
if (setProxyCredentials)
{
remoteService.PreAuthenticate = true;
Normally we would simply want to set the current credentials on the proxy.
remoteService.Credentials = CredentialCache.DefaultCredentials;
}
returnInformation.Append ( remoteService.RetrieveSecurityInformation ());
}
}
// Return the information
return (returnInformation.ToString ());
}
Test Application
No idea why we drew a Win Forms application on the whiteboard and to save time and typing effort, I resorted to the good old console application.
static void CallTest ( string message, bool makeRemoteCall,
bool impersonateCaller,
bool setProxyCredentials,
bool setOtherUser )
{
localhost.ServiceA service = new ConsoleApplicationTest.localhost.ServiceA();
try
{
Console.WriteLine ( message );
Console.WriteLine ( service.RetrieveSecurityInformation ( makeRemoteCall,
impersonateCaller,
setProxyCredentials,
setOtherUser,
null ) );
Console.WriteLine ();
}
catch ( Exception ex )
{
Console.WriteLine ( "Failure: " + ex.ToString() );
Console.WriteLine ();
}
}
static void Main ( string[] args )
{
Program.CallTest ( "Service A call only", false, false, false, false );
Program.CallTest ( "Service A and B - No Impersonation - Setting Proxy",
true, false, true, false );
Program.CallTest ( "Service A and B - No Impersonation - Setting Proxy with other user",
true, false, true, true );
Program.CallTest ( "Service A and B - Impersonation - Setting Proxy",
true, true, true, false );
Program.CallTest ( "Service A and B - No Impersonation - Not Setting Proxy",
true, false, false, false );
Program.CallTest ( "Service A and B - Impersonation - Not Setting Proxy",
true, true, false, false );
Console.ReadLine();
}
Result
The results were as expected as shown in the following copy of the console output, with illustrations inserted.
Service A call only
SERVICE A: CURRENT IDENTTITY INFORMATION --> [ User Name: WILLYXPACER\Research Is Anonymous: False Is Authenticated:True Is Guest: False Is System: False Authentication Type: NTLM ]
SERVICE A: CALLER IDENTTITY INFORMATION --> [ User Name: BBDNET\bbdnet0388 Is Authenticated: True Authentication Type: NTLM ]

Service A and B - No Impersonation - Setting Proxy
SERVICE A: CURRENT IDENTTITY INFORMATION --> [ User Name: WILLYXPACER\Research Is Anonymous: False Is Authenticated:True Is Guest: False Is System: False Authentication Type: NTLM ]
SERVICE A: CALLER IDENTTITY INFORMATION --> [ User Name: BBDNET\bbdnet0388 Is Authenticated: True Authentication Type: NTLM ]
SERVICE B: CURRENT IDENTTITY INFORMATION --> [ User Name: WILLYXPACER\Research Is Anonymous: False Is Authenticated: True Is Guest: False Is System: False Authentication Type: NTLM ]
SERVICE B: CALLER IDENTTITY INFORMATION --> [ User Name: WILLYXPACER\Research Is Authenticated: True Authentication Type: Negotiate ]

Service Aand B - No Impersonation - Setting Proxy with other user
SERVICE A: CURRENT IDENTTITY INFORMATION --> [ User Name: WILLYXPACER\Research Is Anonymous: False Is Authenticated:True Is Guest: False Is System: False Authentication Type: NTLM ]
SERVICE A: CALLER IDENTTITY INFORMATION --> [ User Name: BBDNET\bbdnet0388 Is Authenticated: True Authentication Type: NTLM ]
SERVICE B: CURRENT IDENTTITY INFORMATION --> [ User Name: WILLYXPACER\Impersonator Is Anonymous: False Is Authenticated: True Is Guest: False Is System: False Authentication Type: NTLM ]
SERVICE B: CALLER IDENTTITY INFORMATION --> [ User Name: WILLYXPACER\Impersonator Is Authenticated: True Authentication Type: Negotiate ]

Service A and B - Impersonation - Setting Proxy
SERVICE A: CURRENT IDENTTITY INFORMATION --> [ User Name: WILLYXPACER\Research Is Anonymous: False Is Authenticated:True Is Guest: False Is System: False Authentication Type: NTLM ]
SERVICE A: CALLER IDENTTITY INFORMATION --> [ User Name: BBDNET\bbdnet0388 Is Authenticated: True Authentication Type: NTLM ]
SERVICE A: IMPERSONATED IDENTTITY INFORMATION --> [ User Name: BBDNET\bbdnet0388 Is Anonymous: False Is Authenticated: True Is Guest: False Is System: False Authentication Type: Kerberos ]
SERVICE B: CURRENT IDENTTITY INFORMATION --> [ User Name: BBDNET\bbdnet0388 Is Anonymous: False Is Authenticated: True Is Guest: False Is System: False Authentication Type: Kerberos ]
SERVICE B: CALLER IDENTTITY INFORMATION --> [ User Name: BBDNET\bbdnet0388 Is Authenticated: True Authentication Type: Negotiate ]

Service A and B - No Impersonation - Not Setting Proxy
Failure: System.Web.Services.Protocols.SoapException: System.Web.Services.Protocols.SoapException: Server was unable to process request. ---> System.Net.WebException: The request failed with HTTP status 401: Access Denied.
at System.Web.Services.Protocols.SoapHttpClientProtocol.ReadResponse(SoapClientMessage message, WebResponse response, Stream responseStream, Boolean asyncCall)
at System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String methodName, Object[] parameters)
at ServiceB.ServiceB.RetrieveSecurityInformation() in c:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\servicea\2f181cf1\8f3f0660\App_WebReferences.jqkarxx8.0.cs:line 89
at Service.RetrieveSecurityInformation(Boolean makeRemoteCall, Boolean impersonateCaller, Boolean setProxyCredentials, Boolean setOtherUser, String url) in d:\BBD\Working\SecurityPoC\ServiceA\App_Code\Service.cs:line 125
--- End of inner exception stack trace ---
at System.Web.Services.Protocols.SoapHttpClientProtocol.ReadResponse(SoapClientMessage message, WebResponse response, Stream responseStream, Boolean asyncCall)
at System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String methodName, Object[] parameters)
at ConsoleApplicationTest.localhost.ServiceA.RetrieveSecurityInformation(Boolean makeRemoteCall, Boolean impersonateCaller, Boolean setProxyCredentials, Boolean setOtherUser, String url) in D:\BBD\Working\SecurityPoC\ConsoleApplicationTest\Web References\localhost\Reference.cs:line 142
at ConsoleApplicationTest.Program.CallTest(String message, Boolean makeRemoteCall, Boolean impersonateCaller, Boolean setProxyCredentials, Boolean setOtherUser) in D:\BBD\Working\SecurityPoC\ConsoleApplicationTest\Program.cs:line 22
Service A and B - Impersonation - Not Setting Proxy
Failure: System.Web.Services.Protocols.SoapException: System.Web.Services.Protocols.SoapException: Server was unable to process request. ---> System.Net.WebException: The request failed with HTTP status 401: Access Denied.
at System.Web.Services.Protocols.SoapHttpClientProtocol.ReadResponse(SoapClientMessage message, WebResponse response, Stream responseStream, Boolean asyncCall)
at System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String methodName, Object[] parameters)
at ServiceB.ServiceB.RetrieveSecurityInformation() in c:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\servicea\2f181cf1\8f3f0660\App_WebReferences.jqkarxx8.0.cs:line 89
at Service.RetrieveSecurityInformation(Boolean makeRemoteCall, Boolean impersonateCaller, Boolean setProxyCredentials, Boolean setOtherUser, String url) in d:\BBD\Working\SecurityPoC\ServiceA\App_Code\Service.cs:line 104
--- End of inner exception stack trace ---
at System.Web.Services.Protocols.SoapHttpClientProtocol.ReadResponse(SoapClientMessage message, WebResponse response, Stream responseStream, Boolean asyncCall)
at System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String methodName, Object[] parameters)
at ConsoleApplicationTest.localhost.ServiceA.RetrieveSecurityInformation(Boolean makeRemoteCall, Boolean impersonateCaller, Boolean setProxyCredentials, Boolean setOtherUser, String url) in D:\BBD\Working\SecurityPoC\ConsoleApplicationTest\Web References\localhost\Reference.cs:line 142
at ConsoleApplicationTest.Program.CallTest(String message, Boolean makeRemoteCall, Boolean impersonateCaller, Boolean setProxyCredentials, Boolean setOtherUser) in D:\BBD\Working\SecurityPoC\ConsoleApplicationTest\Program.cs:line 22
In another nutshell … this is ending up to be quite a nutty proof of concept … the tests demonstrate the following:
|
Test |
Notes |
|
Service A call only |
We are able to identify the host identity and calling identity. |
|
A and B - No Impersonation - Setting Proxy |
We can call another service passing the host identity to the service proxy and service. |
|
Service A and B - No Impersonation - Setting Proxy with other user |
We can call another service passing new credentials. |
|
Service A and B - Impersonation - Setting Proxy |
We can impersonate the calling user and pass the credentials to the service proxy and service. |
|
Service A and B - No Impersonation - Not Setting Proxy |
Not setting the proxy credentials results in unhappiness … |
|
"Service A and B - Impersonation - Not Setting Proxy" |
Not setting the proxy credentials results in unhappiness … |
To conclude we hosted web service 2 on another machine and the results changed as follows:
-
Service A and B – No Impersonation – Setting Proxy … Test also failed with Access Denied, HTTP Status 401.
-
Service A and B – No Impersonation – Setting Proxy with other user … Test also failed with Access Denied, HTTP Status 401.
We needed to make the following changes, which are actually quite obvious, but often overseen:
-
WILLYXPACER\Research defined for ServiceA is a local user account. As soon as the request leaves the comfort of the WILLYXPACER, the credentials are not going to find much favour on the remote service. Changing the configuration to use a domain account as follows resolved this issue:
<identity impersonate="true" userName="bbdnet\research" password="password" />
<!--identity impersonate="true" userName="willyxpacer\research" password="nicetry" /-->
For machine hopping … also refered to as delegation … Kerberos is therefore our ally and NTLM a disgruntled localist. .
-
WILLYXPACER\Impersonator used to create a new network credential is also local and suffered the same fate as the one above. The following change was made:
NetworkCredential credential = new NetworkCredential ( "impersonate", "password", "bbdnet" );
//NetworkCredential credential = new NetworkCredential ( "impersonator", "password", "willyxpacer" );
An extract from MSDN in terms of authentication types and delegation as discussed in this PoC.
{ I quote:
|
Authentication Type |
Can Delegate |
Notes |
|
Anonymous |
Depends |
If the anonymous account (by default IUSR_MACHINE) is configured in IIS as a local account, it cannot be delegated unless the local (Web server) and remote computer have identical local accounts (with matching user names and passwords). If the anonymous account is a domain account it can be delegated. |
|
Basic |
Yes |
If Basic authentication is used with local accounts, it can be delegated if the local accounts on the local and remote computers are identical. Domain accounts can also be delegated. |
|
Digest |
No |
|
|
Integrated Windows |
Depends |
Integrated Windows authentication either results in NTLM or Kerberos (depending upon the version of operating system on client and server computer). NTLM does not support delegation. Kerberos supports delegation with the appropriate Active Directory configuration. *SEE NOTE |
|
Client Certificates |
Depends |
Can be delegated if used with IIS certificate mapping and the certificate is mapped to a local account that is duplicated on the remote computer or is mapped to a domain account. This works because the credentials for the mapped account are stored on the local server and are used to create an Interactive logon session (which has network credentials). Active Directory certificate mapping does not support delegation. |
*NOTE
If your application runs under the Network Service account, you need to configure your computer account in Active Directory to be trusted for delegation.
If your application runs under a custom domain account, you need to configure your domain account in Active Directory to be trusted for delegation. You must also register a service principal name in Active Directory to associate the domain account with the HTTP service on your Web server.
… end quote. }
Now that the security discussion is concluded, we can take this opportunity to ensure that Middle-Earth settles down once again.