1 //------------------------------------------------------------------------------
2 // <copyright file="_AuthenticationState.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
5 //------------------------------------------------------------------------------
8 using System.Collections;
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;
16 using System.Text.RegularExpressions;
17 using System.Threading;
18 using System.Globalization;
19 using System.Net.Security;
22 /// <para>Used by HttpWebRequest to syncronize and orchestrate authentication<para>
24 internal class AuthenticationState {
26 // true if we already attempted pre-authentication regardless if it has been
30 private bool TriedPreAuth;
32 internal Authorization Authorization;
34 internal IAuthenticationModule Module;
36 // used to request a special connection for NTLM
37 internal string UniqueGroupId;
39 // used to distinguish proxy auth from server auth
40 private bool IsProxyAuth;
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;
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;
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;
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;
61 #endif // !FEATURE_PAL
63 private TransportContext _TransportContext;
64 internal TransportContext TransportContext
66 get { return _TransportContext; }
67 set { _TransportContext = value; }
70 internal HttpResponseHeader AuthenticateHeader {
72 return IsProxyAuth ? HttpResponseHeader.ProxyAuthenticate : HttpResponseHeader.WwwAuthenticate;
75 internal string AuthorizationHeader {
77 return IsProxyAuth ? HttpKnownHeaderNames.ProxyAuthorization : HttpKnownHeaderNames.Authorization;
80 internal HttpStatusCode StatusCodeMatch {
82 return IsProxyAuth ? HttpStatusCode.ProxyAuthenticationRequired : HttpStatusCode.Unauthorized;
86 internal AuthenticationState(bool isProxyAuth) {
87 IsProxyAuth = isProxyAuth;
91 // we need to do this to handle proxies in the correct way before
92 // calling into the AuthenticationManager APIs
94 private void PrepareState(HttpWebRequest httpWebRequest)
96 Uri newUri = IsProxyAuth ? httpWebRequest.ServicePoint.InternalAddress : httpWebRequest.GetRemoteResourceUri();
98 if ((object)ChallengedUri != (object)newUri)
100 if ((object)ChallengedUri == null || (object)ChallengedUri.Scheme != (object)newUri.Scheme || ChallengedUri.Host != newUri.Host || ChallengedUri.Port != newUri.Port)
103 // must be a new server/port/scheme for this auth state, can happen on a redirect
105 ChallengedSpn = null;
107 ChallengedUri = newUri;
109 httpWebRequest.CurrentAuthenticationState = this;
114 internal SpnToken GetComputeSpn(HttpWebRequest httpWebRequest)
116 if (ChallengedSpn != null)
117 return ChallengedSpn;
119 bool trustNewHost = true; // Assume trusted unless proven otherwise
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)
126 if (!IsProxyAuth && (httpWebRequest.ServicePoint.InternalProxyServicePoint || httpWebRequest.UseCustomHost))
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)
131 // Initialize a backup value
132 host = httpWebRequest.ChallengedUri.Host;
133 // This host comes from the request/user, assume Trusted unless proven otherwise.
135 if (httpWebRequest.ChallengedUri.HostNameType != UriHostNameType.IPv6
136 && httpWebRequest.ChallengedUri.HostNameType != UriHostNameType.IPv4
137 && host.IndexOf('.') == -1)
145 if (Dns.TryInternalResolve(host, out result))
147 host = result.HostName;
148 trustNewHost &= result.isTrustedHost; // Can only lose trust
151 catch (Exception exception) {
152 if (NclUtilities.IsFatal(exception)) throw;
153 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::GetComputeSpn() GetHostByName(host) failed:" + ValidationHelper.ToString(exception));
159 // For this cases we already did a DNS lookup
165 host = httpWebRequest.ServicePoint.Hostname;
166 trustNewHost &= httpWebRequest.ServicePoint.IsTrustedHost; // Can only lose trust
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);
173 ChallengedSpn = spnToken;
174 return ChallengedSpn;
177 internal void PreAuthIfNeeded(HttpWebRequest httpWebRequest, ICredentials authInfo) {
179 // attempt to do preauth, if needed
181 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::PreAuthIfNeeded() TriedPreAuth:" + TriedPreAuth.ToString() + " authInfo:" + ValidationHelper.HashString(authInfo));
184 if (authInfo!=null) {
185 PrepareState(httpWebRequest);
186 Authorization preauth = null;
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);
196 catch (Exception exception) {
197 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::PreAuthIfNeeded() PreAuthenticate() returned exception:" + exception.Message);
198 ClearSession(httpWebRequest);
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
209 internal bool AttemptAuthenticate(HttpWebRequest httpWebRequest, ICredentials authInfo) {
211 // Check for previous authentication attempts or the presence of credentials
213 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() httpWebRequest#" + ValidationHelper.HashString(httpWebRequest) + " AuthorizationHeader:" + AuthorizationHeader.ToString());
215 if (Authorization!=null && Authorization.Complete) {
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)
224 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() Authorization!=null Authorization.Complete:" + Authorization.Complete.ToString());
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()
231 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() ProxyAuth cleaning up auth status");
232 ClearAuthReq(httpWebRequest);
237 if (authInfo==null) {
238 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() authInfo==null Authorization#" + ValidationHelper.HashString(Authorization));
242 string challenge = httpWebRequest.AuthHeader(AuthenticateHeader);
244 if (challenge==null) {
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.
252 if (!IsProxyAuth && Authorization!=null && httpWebRequest.ProxyAuthenticationState.Authorization!=null) {
253 httpWebRequest.Headers.Set(AuthorizationHeader, Authorization.Message);
255 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() challenge==null Authorization#" + ValidationHelper.HashString(Authorization));
260 // if the AuthenticationManager throws on Authenticate,
261 // bubble up that Exception to the user
263 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() challenge:" + challenge);
265 PrepareState(httpWebRequest);
267 Authorization = AuthenticationManager.Authenticate(challenge, httpWebRequest, authInfo);
269 catch (Exception exception) {
270 Authorization = null;
271 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::PreAuthIfNeeded() PreAuthenticate() returned exception:" + exception.Message);
272 ClearSession(httpWebRequest);
277 if (Authorization==null) {
278 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() Authorization==null");
281 if (Authorization.Message==null) {
282 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() Authorization.Message==null");
283 Authorization = null;
287 UniqueGroupId = Authorization.ConnectionGroupId;
288 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::AttemptAuthenticate() AuthorizationHeader:" + AuthorizationHeader + " blob: " + Authorization.Message.Length + "bytes Complete:" + Authorization.Complete.ToString());
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
297 httpWebRequest.Headers.Set(AuthorizationHeader, Authorization.Message);
300 Authorization = null;
301 ClearSession(httpWebRequest);
308 internal void ClearAuthReq(HttpWebRequest httpWebRequest) {
310 // if we are authenticating and we're being redirected to
311 // another authentication space then remove the current
312 // authentication header
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);
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.
325 internal void Update(HttpWebRequest httpWebRequest) {
328 // [....]: 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.
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.
338 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::Update() httpWebRequest#" + ValidationHelper.HashString(httpWebRequest) + " Authorization#" + ValidationHelper.HashString(Authorization) + " ResponseStatusCode:" + httpWebRequest.ResponseStatusCode.ToString());
340 if (Authorization!=null) {
342 PrepareState(httpWebRequest);
344 ISessionAuthenticationModule myModule = Module as ISessionAuthenticationModule;
346 if (myModule!=null) {
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.
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));
355 if (!IsProxyAuth && httpWebRequest.ResponseStatusCode==HttpStatusCode.ProxyAuthenticationRequired) {
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
360 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::Update() skipping call to " + myModule.ToString() + ".Update() since we need to reauthenticate with the proxy");
363 bool complete = true;
365 complete = myModule.Update(challenge, httpWebRequest);
366 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::Update() " + myModule.ToString() + ".Update() returned complete:" + complete.ToString());
368 catch (Exception exception) {
369 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::Update() " + myModule.ToString() + ".Update() caught exception:" + exception.Message);
370 ClearSession(httpWebRequest);
373 if ((httpWebRequest.AuthenticationLevel == AuthenticationLevel.MutualAuthRequired) &&
374 (httpWebRequest.CurrentAuthenticationState == null || httpWebRequest.CurrentAuthenticationState.Authorization == null || !httpWebRequest.CurrentAuthenticationState.Authorization.MutuallyAuthenticated))
378 #endif // !FEATURE_PAL
382 Authorization.SetComplete(complete);
388 // If authentication was successful, create binding between
389 // the request and the authorization for future preauthentication
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);
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;
405 #endif // FEATURE_PAL // Security
408 internal void ClearSession(HttpWebRequest httpWebRequest) {
409 PrepareState(httpWebRequest);
410 ISessionAuthenticationModule myModule = Module as ISessionAuthenticationModule;
413 if (myModule!=null) {
415 myModule.ClearSession(httpWebRequest);
417 catch (Exception exception) {
418 if (NclUtilities.IsFatal(exception)) throw;
420 GlobalLog.Print("AuthenticationState#" + ValidationHelper.HashString(this) + "::ClearSession() " + myModule.ToString() + ".Update() caught exception:" + exception.Message);