Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit 0b989eb

Browse files
author
David Shulman
committed
Fix Kerberos/NTLM for multiple domain/realm environments in Linux
This PR addresses some issues reported by a customer (not thru GitHub). They noticed that multiple domain scenarios were working in .NET Core 2.0 but broke in .NET Core 2.1 on Linux While the previous PR #35383 solved the Negotiate Kerberos to NTLM fallback issue, it added more complexity than necessary. The retry logic I added wasn't really necessary because the original code that used GSS_KRB5_NT_PRINCIPAL_NAME format for the target name was wrong. That logic only works for pure Kerberos environments and doesn't handle domain or realm referrals. So, it only handles the default Kerberos realm on the the single Linux client. In addition, using GSS_KRB5_NT_PRINCIPAL_NAME defeats the logic of the SPNEGO mechanism. That is why I originally needed to add the retry logic using GSS_C_NT_HOSTBASED_SERVICE format for the target name. The multiple domain/realm scenario worked in .NET Core 2.0 because it used CurlHandler. And libcurl always uses GSS_C_NT_HOSTBASED_SERVICE format for target name. This PR reworks the logic to use the GSS_C_NT_HOSTBASED_SERVICE format. It also removes the now unneeded retry logic. I tested this against Windows and Linux as well as pure Kerberos and Kerberos to NTLM fallback (using SPNEGO). I added more tests. These tests are currently part of the enterprise scenario tests we are building. They are activated via environment variables. By definining all of the environment variables below, I am able to run the System.Net.Security and System.Net.Http enterprise-scenario tests. Both SocketsHttpHandler in System.Net.Http and NegotiateStream in System.Net.Security use the same common GSS-API logic in CoreFx. Define domain-joined server remote endpoint: * COREFX_NET_SECURITY_NEGOSERVERURI * COREFX_DOMAINJOINED_HTTPHOST * COREFX_NET_AD_DOMAINNAME * COREFX_NET_AD_PASSWORD * COREFX_NET_AD_USERNAME Define standalone server remote endpoint: * COREFX_NET_SERVER_PASSWORD * COREFX_NET_SERVER_USERNAME * COREFX_WINDOWSSERVER_HTTPHOST
1 parent 3178158 commit 0b989eb

File tree

7 files changed

+79
-120
lines changed

7 files changed

+79
-120
lines changed

src/Common/src/Interop/Unix/System.Net.Security.Native/Interop.NetSecurityNative.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ internal static extern Status ImportTargetName(
4141
out Status minorStatus,
4242
string inputName,
4343
int inputNameByteCount,
44-
bool isNtlmTarget,
4544
out SafeGssNameHandle outputName);
4645

4746
[DllImport(Interop.Libraries.NetSecurityNative, EntryPoint="NetSecurityNative_ReleaseName")]
@@ -77,9 +76,7 @@ internal static extern Status InitSecContext(
7776
bool isNtlmOnly,
7877
IntPtr cbt,
7978
int cbtSize,
80-
bool isNtlmFallback,
81-
SafeGssNameHandle targetNameKerberos,
82-
SafeGssNameHandle targetNameNtlm,
79+
SafeGssNameHandle targetName,
8380
uint reqFlags,
8481
byte[] inputBytes,
8582
int inputLength,

src/Common/src/Microsoft/Win32/SafeHandles/GssSafeHandles.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@ public static SafeGssNameHandle CreateUser(string name)
3131
return retHandle;
3232
}
3333

34-
public static SafeGssNameHandle CreateTarget(string name, bool isNtlmTarget)
34+
public static SafeGssNameHandle CreateTarget(string name)
3535
{
3636
Debug.Assert(!string.IsNullOrEmpty(name), "Invalid target name passed to SafeGssNameHandle create");
3737
SafeGssNameHandle retHandle;
3838
Interop.NetSecurityNative.Status minorStatus;
3939
Interop.NetSecurityNative.Status status = Interop.NetSecurityNative.ImportTargetName(
40-
out minorStatus, name, Encoding.UTF8.GetByteCount(name), isNtlmTarget, out retHandle);
40+
out minorStatus, name, Encoding.UTF8.GetByteCount(name), out retHandle);
4141

4242
if (status != Interop.NetSecurityNative.Status.GSS_S_COMPLETE)
4343
{

src/Common/src/System/Net/Security/NegotiateStreamPal.Unix.cs

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,7 @@ private static bool GssInitSecurityContext(
9999
SafeGssCredHandle credential,
100100
bool isNtlm,
101101
ChannelBinding channelBinding,
102-
bool isNtlmFallback,
103-
SafeGssNameHandle targetNameKerberos,
104-
SafeGssNameHandle targetNameNtlm,
102+
SafeGssNameHandle targetName,
105103
Interop.NetSecurityNative.GssFlags inFlags,
106104
byte[] buffer,
107105
out byte[] outputBuffer,
@@ -143,9 +141,7 @@ private static bool GssInitSecurityContext(
143141
isNtlm,
144142
cbtAppData,
145143
cbtAppDataSize,
146-
isNtlmFallback,
147-
targetNameKerberos,
148-
targetNameNtlm,
144+
targetName,
149145
(uint)inFlags,
150146
buffer,
151147
(buffer == null) ? 0 : buffer.Length,
@@ -180,7 +176,6 @@ private static SecurityStatusPal EstablishSecurityContext(
180176
ref ContextFlagsPal outFlags)
181177
{
182178
bool isNtlmOnly = credential.IsNtlmOnly;
183-
bool initialContext = false;
184179

185180
if (context == null)
186181
{
@@ -190,7 +185,6 @@ private static SecurityStatusPal EstablishSecurityContext(
190185
NetEventSource.Info(null, $"requested protocol = {protocol}, target = {targetName}");
191186
}
192187

193-
initialContext = true;
194188
context = new SafeDeleteNegoContext(credential, targetName);
195189
}
196190

@@ -207,29 +201,21 @@ private static SecurityStatusPal EstablishSecurityContext(
207201
credential.GssCredential,
208202
isNtlmOnly,
209203
channelBinding,
210-
negoContext.IsNtlmFallback,
211-
negoContext.TargetNameKerberos,
212-
negoContext.TargetNameNtlm,
204+
negoContext.TargetName,
213205
inputFlags,
214206
incomingBlob,
215207
out resultBuffer,
216208
out outputFlags,
217209
out isNtlmUsed);
218210

219-
if (initialContext)
211+
if (done)
220212
{
221213
if (NetEventSource.IsEnabled)
222214
{
223215
string protocol = isNtlmOnly ? "NTLM" : isNtlmUsed ? "SPNEGO-NTLM" : "SPNEGO-Kerberos";
224216
NetEventSource.Info(null, $"actual protocol = {protocol}");
225217
}
226218

227-
// Remember if SPNEGO did a fallback from Kerberos to NTLM while generating the initial context.
228-
if (!isNtlmOnly && isNtlmUsed)
229-
{
230-
negoContext.IsNtlmFallback = true;
231-
}
232-
233219
// Populate protocol used for authentication
234220
negoContext.SetAuthenticationPackage(isNtlmUsed);
235221
}

src/Common/src/System/Net/Security/Unix/SafeDeleteNegoContext.cs

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,13 @@ namespace System.Net.Security
1212
{
1313
internal sealed class SafeDeleteNegoContext : SafeDeleteContext
1414
{
15-
private SafeGssNameHandle _targetNameKerberos;
16-
private SafeGssNameHandle _targetNameNtlm;
15+
private SafeGssNameHandle _targetName;
1716
private SafeGssContextHandle _context;
18-
private bool _isNtlmFallback;
1917
private bool _isNtlmUsed;
20-
21-
public SafeGssNameHandle TargetNameKerberos
22-
{
23-
get { return _targetNameKerberos; }
24-
}
25-
26-
public SafeGssNameHandle TargetNameNtlm
27-
{
28-
get { return _targetNameNtlm; }
29-
}
30-
31-
// Property represents if SPNEGO needed to fall back from Kerberos to NTLM when
32-
// generating initial context token.
33-
public bool IsNtlmFallback
18+
19+
public SafeGssNameHandle TargetName
3420
{
35-
get { return _isNtlmFallback; }
36-
set { _isNtlmFallback = value; }
21+
get { return _targetName; }
3722
}
3823

3924
// Property represents if final protocol negotiated is Ntlm or not.
@@ -53,8 +38,10 @@ public SafeDeleteNegoContext(SafeFreeNegoCredentials credential, string targetNa
5338
Debug.Assert((null != credential), "Null credential in SafeDeleteNegoContext");
5439
try
5540
{
56-
_targetNameKerberos = SafeGssNameHandle.CreateTarget(targetName, isNtlmTarget: false);
57-
_targetNameNtlm = SafeGssNameHandle.CreateTarget(targetName, isNtlmTarget: true);
41+
// Convert any "SERVICE/HOST" style of targetName to use "SERVICE@HOST" style.
42+
// This is because the System.Net.Security.Native GSS-API layer uses
43+
// GSS_C_NT_HOSTBASED_SERVICE format for targetName.
44+
_targetName = SafeGssNameHandle.CreateTarget(targetName.Replace('/', '@'));
5845
}
5946
catch
6047
{
@@ -84,16 +71,10 @@ protected override void Dispose(bool disposing)
8471
_context = null;
8572
}
8673

87-
if (_targetNameKerberos != null)
88-
{
89-
_targetNameKerberos.Dispose();
90-
_targetNameKerberos = null;
91-
}
92-
93-
if (_targetNameNtlm != null)
74+
if (_targetName != null)
9475
{
95-
_targetNameNtlm.Dispose();
96-
_targetNameNtlm = null;
76+
_targetName.Dispose();
77+
_targetName = null;
9778
}
9879
}
9980
base.Dispose(disposing);

src/Native/Unix/System.Net.Security.Native/pal_gssapi.c

Lines changed: 25 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -147,28 +147,15 @@ NetSecurityNative_ImportUserName(uint32_t* minorStatus, char* inputName, uint32_
147147
uint32_t NetSecurityNative_ImportTargetName(uint32_t* minorStatus,
148148
char* inputName,
149149
uint32_t inputNameLen,
150-
uint32_t isNtlmTarget,
151150
GssName** outputName)
152151
{
153152
assert(minorStatus != NULL);
154153
assert(inputName != NULL);
155-
assert(isNtlmTarget == 0 || isNtlmTarget == 1);
156154
assert(outputName != NULL);
157155
assert(*outputName == NULL);
158156

159-
gss_OID nameType;
160-
161-
if (isNtlmTarget)
162-
{
163-
nameType = GSS_C_NT_HOSTBASED_SERVICE;
164-
}
165-
else
166-
{
167-
nameType = (gss_OID)(unsigned long)GSS_KRB5_NT_PRINCIPAL_NAME;
168-
}
169-
170157
GssBuffer inputNameBuffer = {.length = inputNameLen, .value = inputName};
171-
return gss_import_name(minorStatus, &inputNameBuffer, nameType, outputName);
158+
return gss_import_name(minorStatus, &inputNameBuffer, GSS_C_NT_HOSTBASED_SERVICE, outputName);
172159
}
173160

174161
uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus,
@@ -177,9 +164,7 @@ uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus,
177164
uint32_t isNtlm,
178165
void* cbt,
179166
int32_t cbtSize,
180-
int32_t isNtlmFallback,
181-
GssName* targetNameKerberos,
182-
GssName* targetNameNtlm,
167+
GssName* targetName,
183168
uint32_t reqFlags,
184169
uint8_t* inputBytes,
185170
uint32_t inputLength,
@@ -190,9 +175,7 @@ uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus,
190175
assert(minorStatus != NULL);
191176
assert(contextHandle != NULL);
192177
assert(isNtlm == 0 || isNtlm == 1);
193-
assert(isNtlm == 1 || targetNameKerberos != NULL);
194-
assert(isNtlmFallback == 0 || isNtlmFallback == 1);
195-
assert(targetNameNtlm != NULL);
178+
assert(targetName != NULL);
196179
assert(inputBytes != NULL || inputLength == 0);
197180
assert(outBuffer != NULL);
198181
assert(retFlags != NULL);
@@ -203,6 +186,7 @@ uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus,
203186
// Note: *contextHandle is null only in the first call and non-null in the subsequent calls
204187

205188
#if HAVE_GSS_SPNEGO_MECHANISM
189+
gss_OID krbMech = GSS_KRB5_MECHANISM;
206190
gss_OID desiredMech;
207191
if (isNtlm)
208192
{
@@ -212,9 +196,8 @@ uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus,
212196
{
213197
desiredMech = GSS_SPNEGO_MECHANISM;
214198
}
215-
216-
gss_OID krbMech = GSS_KRB5_MECHANISM;
217199
#else
200+
gss_OID krbMech = (gss_OID)(unsigned long)gss_mech_krb5;
218201
gss_OID_desc gss_mech_OID_desc;
219202
if (isNtlm)
220203
{
@@ -226,10 +209,8 @@ uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus,
226209
}
227210

228211
gss_OID desiredMech = &gss_mech_OID_desc;
229-
gss_OID krbMech = (gss_OID)(unsigned long)gss_mech_krb5;
230212
#endif
231213

232-
*isNtlmUsed = 1;
233214
GssBuffer inputToken = {.length = inputLength, .value = inputBytes};
234215
GssBuffer gssBuffer = {.length = 0, .value = NULL};
235216
gss_OID_desc* outmech;
@@ -242,49 +223,31 @@ uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus,
242223
gssCbt.application_data.value = cbt;
243224
}
244225

245-
GssName* targetName;
246-
if (isNtlm || isNtlmFallback)
226+
uint32_t majorStatus = gss_init_sec_context(minorStatus,
227+
claimantCredHandle,
228+
contextHandle,
229+
targetName,
230+
desiredMech,
231+
reqFlags,
232+
0,
233+
(cbt != NULL) ? &gssCbt : GSS_C_NO_CHANNEL_BINDINGS,
234+
&inputToken,
235+
&outmech,
236+
&gssBuffer,
237+
retFlags,
238+
NULL);
239+
240+
if (isNtlm)
247241
{
248-
targetName = targetNameNtlm;
242+
*isNtlmUsed = 1;
249243
}
250-
else
244+
else if (majorStatus == GSS_S_COMPLETE && gss_oid_equal(outmech, krbMech) != 0)
251245
{
252-
targetName = targetNameKerberos;
246+
*isNtlmUsed = 0;
253247
}
254-
255-
uint32_t majorStatus;
256-
uint32_t retryCount = 0;
257-
bool retry;
258-
do
259-
{
260-
majorStatus = gss_init_sec_context(minorStatus,
261-
claimantCredHandle,
262-
contextHandle,
263-
targetName,
264-
desiredMech,
265-
reqFlags,
266-
0,
267-
(cbt != NULL) ? &gssCbt : GSS_C_NO_CHANNEL_BINDINGS,
268-
&inputToken,
269-
&outmech,
270-
&gssBuffer,
271-
retFlags,
272-
NULL);
273-
274-
// If SPNEGO fails to generate optimistic Kerberos token on initial call then fall back to SPNEGO-wrapped NTLM.
275-
retry = false;
276-
if ((majorStatus == GSS_S_FAILURE || majorStatus == GSS_S_BAD_NAMETYPE) &&
277-
*contextHandle == NULL && !isNtlm && ++retryCount <= 1)
278-
{
279-
targetName = targetNameNtlm;
280-
retry = true;
281-
}
282-
} while (retry);
283-
284-
// Outmech can be null when gssntlmssp lib uses NTLM mechanism
285-
if (outmech != NULL && gss_oid_equal(outmech, krbMech) != 0)
248+
else
286249
{
287-
*isNtlmUsed = 0;
250+
*isNtlmUsed = 1;
288251
}
289252

290253
NetSecurityNative_MoveBuffer(&gssBuffer, outBuffer);

src/Native/Unix/System.Net.Security.Native/pal_gssapi.h

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@ Shims the gss_import_name method with nametype = GSS_C_NT_USER_NAME.
8282
DLLEXPORT uint32_t NetSecurityNative_ImportTargetName(uint32_t* minorStatus,
8383
char* inputName,
8484
uint32_t inputNameLen,
85-
uint32_t isNtlmTarget,
8685
GssName** outputName);
8786

8887
/*
@@ -110,9 +109,7 @@ DLLEXPORT uint32_t NetSecurityNative_InitSecContext(uint32_t* minorStatus,
110109
uint32_t isNtlm,
111110
void* cbt,
112111
int32_t cbtSize,
113-
int32_t isNtlmFallback,
114-
GssName* targetNameKerberos,
115-
GssName* targetNameNtlm,
112+
GssName* targetName,
116113
uint32_t reqFlags,
117114
uint8_t* inputBytes,
118115
uint32_t inputLength,

src/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Authentication.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,41 @@ public static IEnumerable<object[]> ServerUsesWindowsAuthentication_MemberData()
522522

523523
private static bool IsNtlmInstalled => Capability.IsNtlmInstalled();
524524
private static bool IsWindowsServerAvailable => !string.IsNullOrEmpty(Configuration.Http.WindowsServerHttpHost);
525+
private static bool IsDomainJoinedServerAvailable => !string.IsNullOrEmpty(Configuration.Http.DomainJoinedHttpHost);
526+
527+
[ConditionalFact(nameof(IsDomainJoinedServerAvailable))]
528+
public async Task Credentials_DomainJoinedServerUsesKerberos_Success()
529+
{
530+
if (IsCurlHandler)
531+
{
532+
throw new SkipTestException("CurlHandler (libCurl) will use existing kinit credential if it exists and ignores explicit credential");
533+
}
534+
535+
using (HttpClientHandler handler = CreateHttpClientHandler())
536+
using (var client = new HttpClient(handler))
537+
{
538+
handler.Credentials = new NetworkCredential(
539+
Configuration.Security.ActiveDirectoryUserName,
540+
Configuration.Security.ActiveDirectoryUserPassword,
541+
Configuration.Security.ActiveDirectoryName);
542+
543+
var request = new HttpRequestMessage();
544+
var server = $"http://{Configuration.Http.DomainJoinedHttpHost}/test/auth/kerberos/showidentity.ashx";
545+
request.RequestUri = new Uri(server);
546+
547+
// Force HTTP/1.1 since both CurlHandler and SocketsHttpHandler have problems with
548+
// HTTP/2.0 and Windows authentication (due to HTTP/2.0 -> HTTP/1.1 downgrade handling).
549+
// Issue #35195 (for SocketsHttpHandler).
550+
request.Version = new Version(1,1);
551+
552+
using (HttpResponseMessage response = await client.SendAsync(request))
553+
{
554+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
555+
string body = await response.Content.ReadAsStringAsync();
556+
_output.WriteLine(body);
557+
}
558+
}
559+
}
525560

526561
[ConditionalTheory(nameof(IsNtlmInstalled), nameof(IsWindowsServerAvailable))]
527562
[MemberData(nameof(ServerUsesWindowsAuthentication_MemberData))]

0 commit comments

Comments
 (0)