1 //------------------------------------------------------------------------------ 2 // <copyright file="_NegotiateClient.cs" company="Microsoft"> 3 // Copyright (c) Microsoft Corporation. All rights reserved. 4 // </copyright> 5 //------------------------------------------------------------------------------ 6 7 namespace System.Net { 8 using System.Security.Authentication.ExtendedProtection; 9 10 internal class NegotiateClient : ISessionAuthenticationModule { 11 12 internal const string AuthType = "Negotiate"; 13 private const string negotiateHeader = "Negotiate"; 14 private const string negotiateSignature = "negotiate"; 15 private const string nego2Header = "Nego2"; 16 private const string nego2Signature = "nego2"; 17 Authenticate(string challenge, WebRequest webRequest, ICredentials credentials)18 public Authorization Authenticate(string challenge, WebRequest webRequest, ICredentials credentials) { 19 GlobalLog.Print("NegotiateClient::Authenticate() challenge:[" + ValidationHelper.ToString(challenge) + "] webRequest#" + ValidationHelper.HashString(webRequest) + " credentials#" + ValidationHelper.HashString(credentials) + " calling DoAuthenticate()"); 20 return DoAuthenticate(challenge, webRequest, credentials, false); 21 } 22 DoAuthenticate(string challenge, WebRequest webRequest, ICredentials credentials, bool preAuthenticate)23 private Authorization DoAuthenticate(string challenge, WebRequest webRequest, ICredentials credentials, bool preAuthenticate) { 24 GlobalLog.Print("NegotiateClient::DoAuthenticate() challenge:[" + ValidationHelper.ToString(challenge) + "] webRequest#" + ValidationHelper.HashString(webRequest) + " credentials#" + ValidationHelper.HashString(credentials) + " preAuthenticate:" + preAuthenticate.ToString()); 25 26 GlobalLog.Assert(credentials != null, "NegotiateClient::DoAuthenticate()|credentials == null"); 27 if (credentials == null) { 28 return null; 29 } 30 31 HttpWebRequest httpWebRequest = webRequest as HttpWebRequest; 32 33 GlobalLog.Assert(httpWebRequest != null, "NegotiateClient::DoAuthenticate()|httpWebRequest == null"); 34 GlobalLog.Assert(httpWebRequest.ChallengedUri != null, "NegotiateClient::DoAuthenticate()|httpWebRequest.ChallengedUri == null"); 35 36 NTAuthentication authSession = null; 37 string incoming = null; 38 bool useNego2 = false; // In case of pre-auth we always use "Negotiate", never "Nego2". 39 40 if (!preAuthenticate) { 41 int index = GetSignatureIndex(challenge, out useNego2); 42 if (index < 0) { 43 return null; 44 } 45 46 int blobBegin = index + (useNego2 ? nego2Signature.Length : negotiateSignature.Length); 47 48 // 49 // there may be multiple challenges. If the next character after the 50 // package name is not a comma then it is challenge data 51 // 52 53 if (challenge.Length > blobBegin && challenge[blobBegin] != ',') { 54 ++blobBegin; 55 } 56 else { 57 index = -1; 58 } 59 60 if (index >= 0 && challenge.Length > blobBegin) 61 { 62 // Strip other modules information in case of multiple challenges 63 // i.e do not take ", NTLM" as part of the following Negotiate blob 64 // Negotiate TlRMTVNTUAACAAAADgAOADgAAAA1wo ... MAbwBmAHQALgBjAG8AbQAAAAAA,NTLM 65 index = challenge.IndexOf(',', blobBegin); 66 if (index != -1) 67 incoming = challenge.Substring(blobBegin, index - blobBegin); 68 else 69 incoming = challenge.Substring(blobBegin); 70 } 71 72 authSession = httpWebRequest.CurrentAuthenticationState.GetSecurityContext(this); 73 GlobalLog.Print("NegotiateClient::DoAuthenticate() key:" + ValidationHelper.HashString(httpWebRequest.CurrentAuthenticationState) + " retrieved authSession:" + ValidationHelper.HashString(authSession)); 74 } 75 76 if (authSession==null) 77 { 78 // Credentials are always set for "Negotiate", never for "Nego2". A customer shouldn't even know 79 // about "Nego2". 80 NetworkCredential NC = credentials.GetCredential(httpWebRequest.ChallengedUri, negotiateSignature); 81 GlobalLog.Print("NegotiateClient::DoAuthenticate() GetCredential() returns:" + ValidationHelper.ToString(NC)); 82 83 string username = string.Empty; 84 if (NC == null || (!(NC is SystemNetworkCredential) && (username = NC.InternalGetUserName()).Length == 0)) 85 { 86 return null; 87 } 88 89 ICredentialPolicy policy = AuthenticationManager.CredentialPolicy; 90 if (policy != null && !policy.ShouldSendCredential(httpWebRequest.ChallengedUri, httpWebRequest, NC, this)) 91 return null; 92 93 SpnToken spn = httpWebRequest.CurrentAuthenticationState.GetComputeSpn(httpWebRequest); 94 GlobalLog.Print("NegotiateClient::Authenticate() ChallengedSpn:" + ValidationHelper.ToString(spn)); 95 96 ChannelBinding binding = null; 97 if (httpWebRequest.CurrentAuthenticationState.TransportContext != null) 98 { 99 binding = httpWebRequest.CurrentAuthenticationState.TransportContext.GetChannelBinding(ChannelBindingKind.Endpoint); 100 } 101 102 authSession = 103 new NTAuthentication( 104 AuthType, 105 NC, 106 spn, 107 httpWebRequest, 108 binding); 109 110 GlobalLog.Print("NegotiateClient::DoAuthenticate() setting SecurityContext for:" + ValidationHelper.HashString(httpWebRequest.CurrentAuthenticationState) + " to authSession:" + ValidationHelper.HashString(authSession)); 111 httpWebRequest.CurrentAuthenticationState.SetSecurityContext(authSession, this); 112 } 113 114 string clientResponse = authSession.GetOutgoingBlob(incoming); 115 if (clientResponse==null) { 116 return null; 117 } 118 119 bool canShareConnection = httpWebRequest.UnsafeOrProxyAuthenticatedConnectionSharing; 120 if (canShareConnection) { 121 httpWebRequest.LockConnection = true; 122 } 123 124 // this is the first leg of an NTLM handshake, 125 // set the NtlmKeepAlive override *STRICTLY* only in this case. 126 httpWebRequest.NtlmKeepAlive = incoming==null && authSession.IsValidContext && !authSession.IsKerberos; 127 128 // If we received a "Nego2" header value from the server, we'll respond with "Nego2" in the "Authorization" 129 // header. If the server sent a "Negotiate" header value or if pre-authenticate is used (i.e. the auth blob 130 // is sent with the first request), we send "Negotiate" in the "Authorization" header. 131 return AuthenticationManager.GetGroupAuthorization(this, (useNego2 ? nego2Header : negotiateHeader) + 132 " " + clientResponse, authSession.IsCompleted, authSession, canShareConnection, authSession.IsKerberos); 133 } 134 135 public bool CanPreAuthenticate { 136 get { 137 return true; 138 } 139 } 140 PreAuthenticate(WebRequest webRequest, ICredentials credentials)141 public Authorization PreAuthenticate(WebRequest webRequest, ICredentials credentials) { 142 GlobalLog.Print("NegotiateClient::PreAuthenticate() webRequest#" + ValidationHelper.HashString(webRequest) + " credentials#" + ValidationHelper.HashString(credentials) + " calling DoAuthenticate()"); 143 return DoAuthenticate(null, webRequest, credentials, true); 144 } 145 146 public string AuthenticationType { 147 get { 148 return AuthType; 149 } 150 } 151 152 // 153 // called when getting the final blob on the 200 OK from the server 154 // Update(string challenge, WebRequest webRequest)155 public bool Update(string challenge, WebRequest webRequest) { 156 GlobalLog.Print("NegotiateClient::Update(): " + challenge); 157 158 HttpWebRequest httpWebRequest = webRequest as HttpWebRequest; 159 160 GlobalLog.Assert(httpWebRequest != null, "NegotiateClient::Update()|httpWebRequest == null"); 161 GlobalLog.Assert(httpWebRequest.ChallengedUri != null, "NegotiateClient::Update()|httpWebRequest.ChallengedUri == null"); 162 163 // 164 // try to retrieve the state of the ongoing handshake 165 // 166 167 NTAuthentication authSession = httpWebRequest.CurrentAuthenticationState.GetSecurityContext(this); 168 GlobalLog.Print("NegotiateClient::Update() key:" + ValidationHelper.HashString(httpWebRequest.CurrentAuthenticationState) + " retrieved authSession:" + ValidationHelper.HashString(authSession)); 169 170 if (authSession==null) { 171 GlobalLog.Print("NegotiateClient::Update() null session returning true"); 172 return true; 173 } 174 175 GlobalLog.Print("NegotiateClient::Update() authSession.IsCompleted:" + authSession.IsCompleted.ToString()); 176 177 if (!authSession.IsCompleted && httpWebRequest.CurrentAuthenticationState.StatusCodeMatch==httpWebRequest.ResponseStatusCode) { 178 GlobalLog.Print("NegotiateClient::Update() still handshaking (based on status code) returning false"); 179 return false; 180 } 181 182 // now possibly close the ConnectionGroup after authentication is done. 183 if (!httpWebRequest.UnsafeOrProxyAuthenticatedConnectionSharing) { 184 GlobalLog.Print("NegotiateClient::Update() releasing ConnectionGroup:" + httpWebRequest.GetConnectionGroupLine()); 185 httpWebRequest.ServicePoint.ReleaseConnectionGroup(httpWebRequest.GetConnectionGroupLine()); 186 } 187 188 // 189 // the whole point here is to close the Security Context (this will complete the authentication handshake 190 // with server authentication for schemese that support it such as Kerberos) 191 // 192 bool useNego2 = true; 193 int index = challenge==null ? -1 : GetSignatureIndex(challenge, out useNego2); 194 if (index>=0) { 195 int blobBegin = index + (useNego2 ? nego2Signature.Length : negotiateSignature.Length); 196 string incoming = null; 197 198 // 199 // there may be multiple challenges. If the next character after the 200 // package name is not a comma then it is challenge data 201 // 202 if (challenge.Length > blobBegin && challenge[blobBegin] != ',') { 203 ++blobBegin; 204 } else { 205 index = -1; 206 } 207 if (index >= 0 && challenge.Length > blobBegin) { 208 incoming = challenge.Substring(blobBegin); 209 } 210 GlobalLog.Print("NegotiateClient::Update() this must be a final incoming blob:[" + ValidationHelper.ToString(incoming) + "]"); 211 string clientResponse = authSession.GetOutgoingBlob(incoming); 212 httpWebRequest.CurrentAuthenticationState.Authorization.MutuallyAuthenticated = authSession.IsMutualAuthFlag; 213 GlobalLog.Print("NegotiateClient::Update() GetOutgoingBlob() returns clientResponse:[" + ValidationHelper.ToString(clientResponse) + "] IsCompleted:" + authSession.IsCompleted.ToString()); 214 } 215 216 // Extract the CBT we used and cache it for future requests that want to do preauth 217 httpWebRequest.ServicePoint.SetCachedChannelBinding(httpWebRequest.ChallengedUri, authSession.ChannelBinding); 218 219 GlobalLog.Print("NegotiateClient::Update() session removed and ConnectionGroup released returning true"); 220 ClearSession(httpWebRequest); 221 return true; 222 } 223 ClearSession(WebRequest webRequest)224 public void ClearSession(WebRequest webRequest) { 225 HttpWebRequest httpWebRequest = webRequest as HttpWebRequest; 226 GlobalLog.Assert(httpWebRequest != null, "NegotiateClient::ClearSession()|httpWebRequest == null"); 227 httpWebRequest.CurrentAuthenticationState.ClearSession(); 228 } 229 230 public bool CanUseDefaultCredentials { 231 get { 232 return true; 233 } 234 } 235 GetSignatureIndex(string challenge, out bool useNego2)236 private static int GetSignatureIndex(string challenge, out bool useNego2) { 237 // Negotiate supports two header fields "Nego2" and "Negotiate". If we find "Nego2" we use it, 238 // otherwise we fall back to "Negotiate" (if available). 239 useNego2 = true; 240 241 int index = -1; 242 243 // Consider Nego2 headers only on Win7 and later. Older OS version don't support LiveSSP. 244 if (ComNetOS.IsWin7orLater) { 245 index = AuthenticationManager.FindSubstringNotInQuotes(challenge, nego2Signature); 246 } 247 248 if (index < 0) { 249 useNego2 = false; 250 index = AuthenticationManager.FindSubstringNotInQuotes(challenge, negotiateSignature); 251 } 252 return index; 253 } 254 }; // class NegotiateClient 255 256 257 } // namespace System.Net 258