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