Adding reference source for System.Net
[mono.git] / mcs / class / referencesource / System / net / System / Net / _AuthenticationState.cs
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
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
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
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         //
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         //
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         //
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         //
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
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         //
325         internal void Update(HttpWebRequest httpWebRequest) {
326             //
327             // RAID#86753
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.
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
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
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 }