Finally! An 4.0 XBAP can call a Windows Authenticated WCF Service! Part 2
In the first part to this subject, I wrote about the problem I ran into, the hotfix from Microsoft, the hotfix’s failure, and finally Microsoft’s promise to fix the bug in .Net 4.0. Now that 4.0 is in RC mode and proved itself to fix this problem, I’m going to take a look at the source code to see just how it did fix this problem. But before I look at the 4.0 source code, I’m going to look at the 3.5 pre-hotfix source code and then the 3.5 post hotfix source code to get a feel for what Microsoft was doing which caused my problem. By the way, I’m using RedGate’s Reflector product to look at the source code.
.Net 3.5 Service Pack 1, Pre-Hotfix
The exception came from HttpChannelFactor.GetConnectionGroupName():
[SecurityTreatAsSafe, SecurityCritical] private string GetConnectionGroupName(HttpWebRequest httpWebRequest, NetworkCredential credential, AuthenticationLevel authenticationLevel, TokenImpersonationLevel impersonationLevel, SecurityTokenContainer clientCertificateToken) { if (this.credentialHashCache == null) { lock (base.ThisLock) { if (this.credentialHashCache == null) { this.credentialHashCache = new MruCache<string, string>(5); } } } string inputString = TransferModeHelper.IsRequestStreamed(this.TransferMode) ? "streamed" : string.Empty; if (AuthenticationSchemesHelper.IsWindowsAuth(this.AuthenticationScheme)) { httpWebRequest.UnsafeAuthenticatedConnectionSharing = true; inputString = this.AppendWindowsAuthenticationInfo(inputString, credential, authenticationLevel, impersonationLevel); } inputString = this.OnGetConnectionGroupPrefix(httpWebRequest, clientCertificateToken) + inputString; string str3 = null; if (!string.IsNullOrEmpty(inputString)) { lock (this.credentialHashCache) { if (!this.credentialHashCache.TryGetValue(inputString, out str3)) { byte[] bytes = new UTF8Encoding().GetBytes(inputString); str3 = Convert.ToBase64String(this.HashAlgorithm.ComputeHash(bytes)); this.credentialHashCache.Add(inputString, str3); } } } return str3; }
The GetConnectionGroupName() function is attempting to set the UnsafeAuthenticatedConnectionSharing property and according to the MSDN reference for the property, an application must have unrestricted web permission to set it. However I used the Intranet Zone permission set for my XBAP which did not have the web permission security privlege and so this is why the above exception was thrown. I did not dig into the actual source code when this problem arose last summer, I simply opened a ticket with Microsoft because I figured that I should be able to call a WCF service secured with Windows Integrated security from an XBAP. After a few days, I received feedback from Microsoft that they had released a hotfix.
.Net 3.5 Service Pack 1, Post-Hotfix
Here is how Microsoft fixed the above problem in the hotfix. The below code is the same HttpChannelFactory.GetConnectionGroupName() function shown above except that it has the hotfix changes in it:
[SecurityCritical, SecurityTreatAsSafe] private string GetConnectionGroupName(HttpWebRequest httpWebRequest, NetworkCredential credential, AuthenticationLevel authenticationLevel, TokenImpersonationLevel impersonationLevel, SecurityTokenContainer clientCertificateToken) { if (this.credentialHashCache == null) { lock (base.ThisLock) { if (this.credentialHashCache == null) { this.credentialHashCache = new MruCache<string, string>(5); } } } string inputString = TransferModeHelper.IsRequestStreamed(this.TransferMode) ? "streamed" : string.Empty; if (AuthenticationSchemesHelper.IsWindowsAuth(this.AuthenticationScheme)) { if (!httpWebRequestWebPermissionDenied) { try { httpWebRequest.UnsafeAuthenticatedConnectionSharing = true; } catch (SecurityException) { httpWebRequestWebPermissionDenied = true; } } inputString = this.AppendWindowsAuthenticationInfo(inputString, credential, authenticationLevel, impersonationLevel); } inputString = this.OnGetConnectionGroupPrefix(httpWebRequest, clientCertificateToken) + inputString; string str3 = null; if (!string.IsNullOrEmpty(inputString)) { lock (this.credentialHashCache) { if (!this.credentialHashCache.TryGetValue(inputString, out str3)) { byte[] bytes = new UTF8Encoding().GetBytes(inputString); str3 = Convert.ToBase64String(this.HashAlgorithm.ComputeHash(bytes)); this.credentialHashCache.Add(inputString, str3); } } } return str3; }
The httpWebRequestWebPermissionDenied flag is checked to guard against setting the UnsafeAuthenticatedConnectionSharing property. So if this flag is set, the code will not try to set the connection sharing property. However, where is the httpWebRequestWebPermissionDenied flag set? It is set to true if an exception is thrown in the above code on line 25 and it is initialized to false in the constructor but the only other place it is set is in the OnOpen() function:
protected override void OnOpen(TimeSpan timeout) { if (this.IsSecurityTokenManagerRequired()) { this.InitializeSecurityTokenManager(); } if (this.AllowCookies) { this.cookieContainer = new CookieContainer(); } if (!httpWebRequestWebPermissionDenied && (HttpWebRequest.DefaultMaximumErrorResponseLength != -1)) { int num; if (this.MaxBufferSize >= 0x7ffffbff) { num = -1; } else { num = this.MaxBufferSize / 0x400; if ((num * 0x400) < this.MaxBufferSize) { num++; } } if ((num == -1) || (num > HttpWebRequest.DefaultMaximumErrorResponseLength)) { try { HttpWebRequest.DefaultMaximumErrorResponseLength = num; } catch (SecurityException exception) { httpWebRequestWebPermissionDenied = true; if (DiagnosticUtility.ShouldTraceWarning) { DiagnosticUtility.ExceptionUtility.TraceHandledException(exception, TraceEventType.Warning); } } } } }
This function uses pretty much the same logic to set the httpWebRequestWebPermissionDenied flag. If a SecurityException is thrown, then the flag is set to true. That seems like a quick and dirty way to set the permission flag’s value and I guess it works but it seems to me that it should be set during class initialization by reading the XBAP’s associated manifest file to see if that permission is in the XBAP’s permission set. I looked at the .Net 4.0 code to see if the flag was initialized in some way but found that it uses the same logic to figure out if the XBAP doesn’t have web permission. Strange, I thought there’d be a more elegant way.
However ugly the above hotfix is, it did get the code past that point and on down the callstack. But farther down in the callstack another exception was thrown:
This next exception was thrown because SecurityUtils.AppendWindowsAuthenticationInfo() called WindowsIdentity.GetCurrent():
[SecurityCritical] internal static string AppendWindowsAuthenticationInfo(string inputString, NetworkCredential credential, AuthenticationLevel authenticationLevel, TokenImpersonationLevel impersonationLevel) { if (IsDefaultNetworkCredential(credential)) { using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) { SecurityIdentifier user = identity.User; return (inputString + "\0" + user.Value + "\0" + AuthenticationLevelHelper.ToString(authenticationLevel) + "\0" + TokenImpersonationLevelHelper.ToString(impersonationLevel)); } } return (inputString + "\0" + NetworkCredentialHelper.UnsafeGetDomain(credential) + "\0" + NetworkCredentialHelper.UnsafeGetUsername(credential) + "\0" + NetworkCredentialHelper.UnsafeGetPassword(credential) + "\0" + AuthenticationLevelHelper.ToString(authenticationLevel) + "\0" + TokenImpersonationLevelHelper.ToString(impersonationLevel)); }
However, in order to call WindowsIdentity.GetCurrent, the app must have SecurityPermissionFlag.ControlPrincipal as the WindowsIdentity.GetCurrent() method demands it:
[SecurityPermission(SecurityAction.Demand, Flags=SecurityPermissionFlag.ControlPrincipal)] public static WindowsIdentity GetCurrent() { return GetCurrentInternal(TokenAccessLevels.MaximumAllowed, false); }
Since my clickonce XBAP did not have this security, this Demand caused another exception.
.Net 4.0
The .Net 4.0 code was the same as the hotfix code for dealing with the UnsafeAuthenticatedConnectionSharing property problem. However it handled the WindowsIdentity.GetCurrent() somewhat differently. Here is the SecurityUtils.AppendWindowsAuthenticationInfo() function in .Net 4.0:
</pre> [SecurityCritical] internal static string AppendWindowsAuthenticationInfo(string inputString, NetworkCredential credential, AuthenticationLevel authenticationLevel, TokenImpersonationLevel impersonationLevel) { if (IsDefaultNetworkCredential(credential)) { string str = UnsafeGetCurrentUserSidAsString(); return (inputString + "\0" + str + "\0" + AuthenticationLevelHelper.ToString(authenticationLevel) + "\0" + TokenImpersonationLevelHelper.ToString(impersonationLevel)); } return (inputString + "\0" + NetworkCredentialHelper.UnsafeGetDomain(credential) + "\0" + NetworkCredentialHelper.UnsafeGetUsername(credential) + "\0" + NetworkCredentialHelper.UnsafeGetPassword(credential) + "\0" + AuthenticationLevelHelper.ToString(authenticationLevel) + "\0" + TokenImpersonationLevelHelper.ToString(impersonationLevel)); }
The logic for this version of SecurityUtils.AppendWindowsAuthenticationInfo() is about the same as the logic in .Net 3.5 except for the call to the new member UnsafeGetCurrentUserSidAsString() on line 11. The code to this new member function is shown below:
[SecurityCritical, SecurityPermission(SecurityAction.Assert, Flags=SecurityPermissionFlag.ControlPrincipal)] private static string UnsafeGetCurrentUserSidAsString() { using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) { return identity.User.Value; } }
On line 1 of the above code there is an Assert for the ControlPrincipal flag, the same flag which WindowsIdentity.GetCurrent() demands. This Assert will satisfy the stack walk caused by the Demand and no exception will be thrown. So that is how Microsoft fixed this bug in .Net 4.0.