1 //------------------------------------------------------------------------------ 2 // <copyright file="_AuthenticationState.cs" company="Microsoft"> 3 // Copyright (c) Microsoft Corporation. All rights reserved. 4 // </copyright> 5 //------------------------------------------------------------------------------ 6 7 namespace System.Net { 8 using System.Collections; 9 using System.IO; 10 using System.Runtime.Serialization; 11 using System.Security; 12 using System.Security.Authentication.ExtendedProtection; 13 using System.Security.Cryptography.X509Certificates; 14 using System.Security.Permissions; 15 using System.Text; 16 using System.Text.RegularExpressions; 17 using System.Threading; 18 using System.Globalization; 19 using System.Net.Security; 20 21 /// <devdoc> 22 /// <para>Used by HttpWebRequest to syncronize and orchestrate authentication<para> 23 /// </devdoc> 24 internal class AuthenticationState { 25 26 // true if we already attempted pre-authentication regardless if it has been 27 // 1) possible, 28 // 2) succesfull or 29 // 3) unsuccessfull 30 private bool TriedPreAuth; 31 32 internal Authorization Authorization; 33 34 internal IAuthenticationModule Module; 35 36 // used to request a special connection for NTLM 37 internal string UniqueGroupId; 38 39 // used to distinguish proxy auth from server auth 40 private bool IsProxyAuth; 41 42 // the Uri of the host we're authenticating (proxy/server) 43 // used to match entries in the CredentialCache 44 internal Uri ChallengedUri; 45 private SpnToken ChallengedSpn; 46 47 #if !FEATURE_PAL 48 // this is the client's security context for SSPI based authentication 49 // pared with the authentication module that set it. 50 private NTAuthentication SecurityContext; 51 GetSecurityContext(IAuthenticationModule module)52 internal NTAuthentication GetSecurityContext(IAuthenticationModule module) { 53 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::GetSecurityContext(" + module.AuthenticationType + ") returning NTAuthentication#" + ValidationHelper.HashString((object)module==(object)Module ? SecurityContext : null)); 54 return (object)module==(object)Module ? SecurityContext : null; 55 } 56 SetSecurityContext(NTAuthentication securityContext, IAuthenticationModule module)57 internal void SetSecurityContext(NTAuthentication securityContext, IAuthenticationModule module) { 58 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::SetSecurityContext(" + module.AuthenticationType + ") was NTAuthentication#" + ValidationHelper.HashString(SecurityContext) + " now NTAuthentication#" + ValidationHelper.HashString(securityContext)); 59 SecurityContext = securityContext; 60 } 61 #endif // !FEATURE_PAL 62 63 private TransportContext _TransportContext; 64 internal TransportContext TransportContext 65 { 66 get { return _TransportContext; } 67 set { _TransportContext = value; } 68 } 69 70 internal HttpResponseHeader AuthenticateHeader { 71 get { 72 return IsProxyAuth ? HttpResponseHeader.ProxyAuthenticate : HttpResponseHeader.WwwAuthenticate; 73 } 74 } 75 internal string AuthorizationHeader { 76 get { 77 return IsProxyAuth ? HttpKnownHeaderNames.ProxyAuthorization : HttpKnownHeaderNames.Authorization; 78 } 79 } 80 internal HttpStatusCode StatusCodeMatch { 81 get { 82 return IsProxyAuth ? HttpStatusCode.ProxyAuthenticationRequired : HttpStatusCode.Unauthorized; 83 } 84 } 85 AuthenticationState(bool isProxyAuth)86 internal AuthenticationState(bool isProxyAuth) { 87 IsProxyAuth = isProxyAuth; 88 } 89 90 // 91 // we need to do this to handle proxies in the correct way before 92 // calling into the AuthenticationManager APIs 93 // PrepareState(HttpWebRequest httpWebRequest)94 private void PrepareState(HttpWebRequest httpWebRequest) 95 { 96 Uri newUri = IsProxyAuth ? httpWebRequest.ServicePoint.InternalAddress : httpWebRequest.GetRemoteResourceUri(); 97 98 if ((object)ChallengedUri != (object)newUri) 99 { 100 if ((object)ChallengedUri == null || (object)ChallengedUri.Scheme != (object)newUri.Scheme || ChallengedUri.Host != newUri.Host || ChallengedUri.Port != newUri.Port) 101 { 102 // 103 // must be a new server/port/scheme for this auth state, can happen on a redirect 104 // 105 ChallengedSpn = null; 106 } 107 ChallengedUri = newUri; 108 } 109 httpWebRequest.CurrentAuthenticationState = this; 110 } 111 // 112 // 113 // GetComputeSpn(HttpWebRequest httpWebRequest)114 internal SpnToken GetComputeSpn(HttpWebRequest httpWebRequest) 115 { 116 if (ChallengedSpn != null) 117 return ChallengedSpn; 118 119 bool trustNewHost = true; // Assume trusted unless proven otherwise 120 121 string spnKey = httpWebRequest.ChallengedUri.GetParts(UriComponents.Scheme | UriComponents.Host | UriComponents.Port | UriComponents.Path, UriFormat.SafeUnescaped); 122 SpnToken spnToken = AuthenticationManager.SpnDictionary.InternalGet(spnKey); 123 if (spnToken == null || spnToken.Spn == null) 124 { 125 string host; 126 if (!IsProxyAuth && (httpWebRequest.ServicePoint.InternalProxyServicePoint || httpWebRequest.UseCustomHost)) 127 { 128 // Here the NT-Security folks need us to attempt a DNS lookup to figure out 129 // the FQDN. only do the lookup for short names (no IP addresses or DNS names) 130 // 131 // Initialize a backup value 132 host = httpWebRequest.ChallengedUri.Host; 133 // This host comes from the request/user, assume Trusted unless proven otherwise. 134 135 if (httpWebRequest.ChallengedUri.HostNameType != UriHostNameType.IPv6 136 && httpWebRequest.ChallengedUri.HostNameType != UriHostNameType.IPv4 137 && host.IndexOf('.') == -1) 138 { 139 try { 140 // 141 142 143 144 IPHostEntry result; 145 if (Dns.TryInternalResolve(host, out result)) 146 { 147 host = result.HostName; 148 trustNewHost &= result.isTrustedHost; // Can only lose trust 149 } 150 } 151 catch (Exception exception) { 152 if (NclUtilities.IsFatal(exception)) throw; 153 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::GetComputeSpn() GetHostByName(host) failed:" + ValidationHelper.ToString(exception)); 154 } 155 } 156 } 157 else 158 { 159 // For this cases we already did a DNS lookup 160 161 // 162 163 164 165 host = httpWebRequest.ServicePoint.Hostname; 166 trustNewHost &= httpWebRequest.ServicePoint.IsTrustedHost; // Can only lose trust 167 } 168 string spn = "HTTP/" + host; 169 spnKey = httpWebRequest.ChallengedUri.GetParts(UriComponents.SchemeAndServer, UriFormat.SafeUnescaped) + "/"; 170 spnToken = new SpnToken(spn, trustNewHost); 171 AuthenticationManager.SpnDictionary.InternalSet(spnKey, spnToken); 172 } 173 ChallengedSpn = spnToken; 174 return ChallengedSpn; 175 } 176 // PreAuthIfNeeded(HttpWebRequest httpWebRequest, ICredentials authInfo)177 internal void PreAuthIfNeeded(HttpWebRequest httpWebRequest, ICredentials authInfo) { 178 // 179 // attempt to do preauth, if needed 180 // 181 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::PreAuthIfNeeded() TriedPreAuth:" + TriedPreAuth.ToString() + " authInfo:" + ValidationHelper.HashString(authInfo)); 182 if (!TriedPreAuth) { 183 TriedPreAuth = true; 184 if (authInfo!=null) { 185 PrepareState(httpWebRequest); 186 Authorization preauth = null; 187 try { 188 preauth = AuthenticationManager.PreAuthenticate(httpWebRequest, authInfo); 189 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::PreAuthIfNeeded() preauth:" + ValidationHelper.HashString(preauth)); 190 if (preauth!=null && preauth.Message!=null) { 191 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::PreAuthIfNeeded() setting TriedPreAuth to Complete:" + preauth.Complete.ToString()); 192 UniqueGroupId = preauth.ConnectionGroupId; 193 httpWebRequest.Headers.Set(AuthorizationHeader, preauth.Message); 194 } 195 } 196 catch (Exception exception) { 197 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::PreAuthIfNeeded() PreAuthenticate() returned exception:" + exception.Message); 198 ClearSession(httpWebRequest); 199 } 200 } 201 } 202 } 203 204 // 205 // attempts to authenticate the request: 206 // returns true only if it succesfully called into the AuthenticationManager 207 // and got back a valid Authorization and succesfully set the appropriate auth headers 208 // AttemptAuthenticate(HttpWebRequest httpWebRequest, ICredentials authInfo)209 internal bool AttemptAuthenticate(HttpWebRequest httpWebRequest, ICredentials authInfo) { 210 // 211 // Check for previous authentication attempts or the presence of credentials 212 // 213 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() httpWebRequest#" + ValidationHelper.HashString(httpWebRequest) + " AuthorizationHeader:" + AuthorizationHeader.ToString()); 214 215 if (Authorization!=null && Authorization.Complete) { 216 // 217 // here the design gets "dirty". 218 // if this is proxy auth, we might have been challenged by an external 219 // server as well. in this case we will have to clear our previous proxy 220 // auth state before we go any further. this will be broken if the handshake 221 // requires more than one dropped connection (which NTLM is a border case for, 222 // since it droppes the connection on the 1st challenge but not on the second) 223 // 224 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() Authorization!=null Authorization.Complete:" + Authorization.Complete.ToString()); 225 if (IsProxyAuth) { 226 // 227 // so, we got passed a 407 but now we got a 401, the proxy probably 228 // dropped the connection on us so we need to reset our proxy handshake 229 // Consider: this should have been taken care by Update() 230 // 231 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() ProxyAuth cleaning up auth status"); 232 ClearAuthReq(httpWebRequest); 233 } 234 return false; 235 } 236 237 if (authInfo==null) { 238 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() authInfo==null Authorization#" + ValidationHelper.HashString(Authorization)); 239 return false; 240 } 241 242 string challenge = httpWebRequest.AuthHeader(AuthenticateHeader); 243 244 if (challenge==null) { 245 // 246 // the server sent no challenge, but this might be the case 247 // in which we're succeeding an authorization handshake to 248 // a proxy while a handshake with the server is still in progress. 249 // if the handshake with the proxy is complete and we actually have 250 // a handshake with the server in progress we can send the authorization header for the server as well. 251 // 252 if (!IsProxyAuth && Authorization!=null && httpWebRequest.ProxyAuthenticationState.Authorization!=null) { 253 httpWebRequest.Headers.Set(AuthorizationHeader, Authorization.Message); 254 } 255 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() challenge==null Authorization#" + ValidationHelper.HashString(Authorization)); 256 return false; 257 } 258 259 // 260 // if the AuthenticationManager throws on Authenticate, 261 // bubble up that Exception to the user 262 // 263 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() challenge:" + challenge); 264 265 PrepareState(httpWebRequest); 266 try { 267 Authorization = AuthenticationManager.Authenticate(challenge, httpWebRequest, authInfo); 268 } 269 catch (Exception exception) { 270 Authorization = null; 271 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::PreAuthIfNeeded() PreAuthenticate() returned exception:" + exception.Message); 272 ClearSession(httpWebRequest); 273 throw; 274 } 275 276 277 if (Authorization==null) { 278 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() Authorization==null"); 279 return false; 280 } 281 if (Authorization.Message==null) { 282 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() Authorization.Message==null"); 283 Authorization = null; 284 return false; 285 } 286 287 UniqueGroupId = Authorization.ConnectionGroupId; 288 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() AuthorizationHeader:" + AuthorizationHeader + " blob: " + Authorization.Message.Length + "bytes Complete:" + Authorization.Complete.ToString()); 289 290 try { 291 // 292 // a "bad" module could try sending bad characters in the HTTP headers. 293 // catch the exception from WebHeaderCollection.CheckBadChars() 294 // fail the auth process 295 // and return the exception to the user as InnerException 296 // 297 httpWebRequest.Headers.Set(AuthorizationHeader, Authorization.Message); 298 } 299 catch { 300 Authorization = null; 301 ClearSession(httpWebRequest); 302 throw; 303 } 304 305 return true; 306 } 307 ClearAuthReq(HttpWebRequest httpWebRequest)308 internal void ClearAuthReq(HttpWebRequest httpWebRequest) { 309 // 310 // if we are authenticating and we're being redirected to 311 // another authentication space then remove the current 312 // authentication header 313 // 314 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::ClearAuthReq() httpWebRequest#" + ValidationHelper.HashString(httpWebRequest) + " " + AuthorizationHeader.ToString() + ": " + ValidationHelper.ToString(httpWebRequest.Headers[AuthorizationHeader])); 315 TriedPreAuth = false; 316 Authorization = null; 317 UniqueGroupId = null; 318 httpWebRequest.Headers.Remove(AuthorizationHeader); 319 } 320 321 // 322 // gives the IAuthenticationModule a chance to update its internal state. 323 // do any necessary cleanup and update the Complete status of the associated Authorization. 324 // Update(HttpWebRequest httpWebRequest)325 internal void Update(HttpWebRequest httpWebRequest) { 326 // 327 // RAID#86753 328 // Microsoft: this is just a fix for redirection & kerberos. 329 // we need to close the Context and call ISC() again with the final 330 // blob returned from the server. to do this in general 331 // we would probably need to change the IAuthenticationMdule interface and 332 // add this Update() method. for now we just have it internally. 333 // 334 // actually this turns out to be quite handy for 2 more cases: 335 // NTLM auth: we need to clear the connection group after we suceed to prevent leakage. 336 // Digest auth: we need to support stale credentials, if we fail with a 401 and stale is true we need to retry. 337 // 338 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::Update() httpWebRequest#" + ValidationHelper.HashString(httpWebRequest) + " Authorization#" + ValidationHelper.HashString(Authorization) + " ResponseStatusCode:" + httpWebRequest.ResponseStatusCode.ToString()); 339 340 if (Authorization!=null) { 341 342 PrepareState(httpWebRequest); 343 344 ISessionAuthenticationModule myModule = Module as ISessionAuthenticationModule; 345 346 if (myModule!=null) { 347 // 348 // the whole point here is to complete the Security Context. Sometimes, though, 349 // a bad cgi script or a bad server, could miss sending back the final blob. 350 // in this case we won't be able to complete the handshake, but we'll have to clean up anyway. 351 // 352 string challenge = httpWebRequest.AuthHeader(AuthenticateHeader); 353 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::Update() Complete:" + Authorization.Complete.ToString() + " Module:" + ValidationHelper.ToString(Module) + " challenge:" + ValidationHelper.ToString(challenge)); 354 355 if (!IsProxyAuth && httpWebRequest.ResponseStatusCode==HttpStatusCode.ProxyAuthenticationRequired) { 356 // 357 // don't call Update on the module, since there's an ongoing 358 // handshake and we don't need to update any state in such a case 359 // 360 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::Update() skipping call to " + myModule.ToString() + ".Update() since we need to reauthenticate with the proxy"); 361 } 362 else { 363 bool complete = true; 364 try { 365 complete = myModule.Update(challenge, httpWebRequest); 366 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::Update() " + myModule.ToString() + ".Update() returned complete:" + complete.ToString()); 367 } 368 catch (Exception exception) { 369 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::Update() " + myModule.ToString() + ".Update() caught exception:" + exception.Message); 370 ClearSession(httpWebRequest); 371 372 #if !FEATURE_PAL 373 if ((httpWebRequest.AuthenticationLevel == AuthenticationLevel.MutualAuthRequired) && 374 (httpWebRequest.CurrentAuthenticationState == null || httpWebRequest.CurrentAuthenticationState.Authorization == null || !httpWebRequest.CurrentAuthenticationState.Authorization.MutuallyAuthenticated)) 375 { 376 throw; 377 } 378 #endif // !FEATURE_PAL 379 380 } 381 382 Authorization.SetComplete(complete); 383 } 384 385 } 386 387 // 388 // If authentication was successful, create binding between 389 // the request and the authorization for future preauthentication 390 // 391 if (httpWebRequest.PreAuthenticate && Module != null && Authorization.Complete && Module.CanPreAuthenticate && httpWebRequest.ResponseStatusCode != StatusCodeMatch) { 392 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::Update() handshake is Complete calling BindModule()"); 393 AuthenticationManager.BindModule(ChallengedUri, Authorization, Module); 394 } 395 } 396 } 397 ClearSession()398 internal void ClearSession() { 399 #if !FEATURE_PAL // Security 400 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::ClearSession() NTAuthentication#" + ValidationHelper.HashString(SecurityContext)); 401 if (SecurityContext!=null) { 402 SecurityContext.CloseContext(); 403 SecurityContext = null; 404 } 405 #endif // FEATURE_PAL // Security 406 } 407 ClearSession(HttpWebRequest httpWebRequest)408 internal void ClearSession(HttpWebRequest httpWebRequest) { 409 PrepareState(httpWebRequest); 410 ISessionAuthenticationModule myModule = Module as ISessionAuthenticationModule; 411 Module = null; 412 413 if (myModule!=null) { 414 try { 415 myModule.ClearSession(httpWebRequest); 416 } 417 catch (Exception exception) { 418 if (NclUtilities.IsFatal(exception)) throw; 419 420 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::ClearSession() " + myModule.ToString() + ".Update() caught exception:" + exception.Message); 421 } 422 } 423 424 } 425 426 } 427 } 428