public async void Invalid_Token() { using var kerberosExecutor = new KerberosExecutor(_testOutputHelper, "LINUX.CONTOSO.COM"); // Force a non-empty keytab to make macOS happy kerberosExecutor.AddService("HTTP/linux.contoso.com"); await kerberosExecutor.Invoke(() => { NegotiateAuthentication ntAuth = new NegotiateAuthentication(new NegotiateAuthenticationServerOptions { }); // Ask for NegHints byte[] blob = ntAuth.GetOutgoingBlob((ReadOnlySpan <byte>) default, out NegotiateAuthenticationStatusCode statusCode);
public Authorization?Authenticate(string?challenge, NetworkCredential?credential, object sessionCookie, string?spn, ChannelBinding?channelBindingToken) { lock (_sessions) { NegotiateAuthentication?clientContext; if (!_sessions.TryGetValue(sessionCookie, out clientContext)) { if (credential == null) { return(null); } _sessions[sessionCookie] = clientContext = new NegotiateAuthentication( new NegotiateAuthenticationClientOptions { Credential = credential, TargetName = spn, Binding = channelBindingToken }); } NegotiateAuthenticationStatusCode statusCode; string?resp = clientContext.GetOutgoingBlob(challenge, out statusCode); if (statusCode != NegotiateAuthenticationStatusCode.Completed && statusCode != NegotiateAuthenticationStatusCode.ContinueNeeded) { return(null); } if (!clientContext.IsAuthenticated) { return(new Authorization(resp, false)); } else { _sessions.Remove(sessionCookie); clientContext.Dispose(); return(new Authorization(resp, true)); } } }
public Authorization?Authenticate(string?challenge, NetworkCredential?credential, object sessionCookie, string?spn, ChannelBinding?channelBindingToken) { lock (_sessions) { NegotiateAuthentication?clientContext; if (!_sessions.TryGetValue(sessionCookie, out clientContext)) { if (credential == null) { return(null); } ProtectionLevel protectionLevel = ProtectionLevel.Sign; // Workaround for https://github.com/gssapi/gss-ntlmssp/issues/77 // GSSAPI NTLM SSP does not support gss_wrap/gss_unwrap unless confidentiality // is negotiated. if (OperatingSystem.IsLinux()) { protectionLevel = ProtectionLevel.EncryptAndSign; } _sessions[sessionCookie] = clientContext = new NegotiateAuthentication( new NegotiateAuthenticationClientOptions { Credential = credential, TargetName = spn, RequiredProtectionLevel = protectionLevel, Binding = channelBindingToken }); } string?resp = null; NegotiateAuthenticationStatusCode statusCode; if (!clientContext.IsAuthenticated) { // If auth is not yet completed keep producing // challenge responses with GetOutgoingBlob resp = clientContext.GetOutgoingBlob(challenge, out statusCode); if (statusCode != NegotiateAuthenticationStatusCode.Completed && statusCode != NegotiateAuthenticationStatusCode.ContinueNeeded) { return(null); } if (clientContext.IsAuthenticated && resp == null) { resp = "\r\n"; } } else { // If auth completed and still have a challenge then // server may be doing "correct" form of GSSAPI SASL. // Validate incoming and produce outgoing SASL security // layer negotiate message. resp = GetSecurityLayerOutgoingBlob(challenge, clientContext); } return(new Authorization(resp, clientContext.IsAuthenticated)); } }
// Function for SASL security layer negotiation after // authorization completes. // // Returns null for failure, Base64 encoded string on // success. private static string?GetSecurityLayerOutgoingBlob(string?challenge, NegotiateAuthentication clientContext) { // must have a security layer challenge if (challenge == null) { return(null); } // "unwrap" challenge byte[] input = Convert.FromBase64String(challenge); Span <byte> unwrappedChallenge; NegotiateAuthenticationStatusCode statusCode; statusCode = clientContext.UnwrapInPlace(input, out int newOffset, out int newLength, out _); if (statusCode != NegotiateAuthenticationStatusCode.Completed) { return(null); } unwrappedChallenge = input.AsSpan(newOffset, newLength); // Per RFC 2222 Section 7.2.2: // the client should then expect the server to issue a // token in a subsequent challenge. The client passes // this token to GSS_Unwrap and interprets the first // octet of cleartext as a bit-mask specifying the // security layers supported by the server and the // second through fourth octets as the maximum size // output_message to send to the server. // Section 7.2.3 // The security layer and their corresponding bit-masks // are as follows: // 1 No security layer // 2 Integrity protection // Sender calls GSS_Wrap with conf_flag set to FALSE // 4 Privacy protection // Sender calls GSS_Wrap with conf_flag set to TRUE // // Exchange 2007 and our client only support // "No security layer". We verify that the server offers // option to use no security layer and negotiate that if // possible. if (unwrappedChallenge.Length != 4 || (unwrappedChallenge[0] & 1) != 1) { return(null); } // Continuing with RFC 2222 section 7.2.2: // The client then constructs data, with the first octet // containing the bit-mask specifying the selected security // layer, the second through fourth octets containing in // network byte order the maximum size output_message the client // is able to receive, and the remaining octets containing the // authorization identity. // // So now this constructs the "wrapped" response. // let MakeSignature figure out length of output ArrayBufferWriter <byte> outputWriter = new ArrayBufferWriter <byte>(); statusCode = clientContext.Wrap(_saslNoSecurtyLayerToken, outputWriter, false, out _); if (statusCode != NegotiateAuthenticationStatusCode.Completed) { return(null); } // return Base64 encoded string of signed payload return(Convert.ToBase64String(outputWriter.WrittenSpan)); }
private static async Task <HttpResponseMessage> SendWithNtAuthAsync(HttpRequestMessage request, Uri authUri, bool async, ICredentials credentials, bool isProxyAuth, HttpConnection connection, HttpConnectionPool connectionPool, CancellationToken cancellationToken) { HttpResponseMessage response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false); if (!isProxyAuth && connection.Kind == HttpConnectionKind.Proxy && !ProxySupportsConnectionAuth(response)) { // Proxy didn't indicate that it supports connection-based auth, so we can't proceed. if (NetEventSource.Log.IsEnabled()) { NetEventSource.Error(connection, $"Proxy doesn't support connection-based auth, uri={authUri}"); } return(response); } if (TryGetAuthenticationChallenge(response, isProxyAuth, authUri, credentials, out AuthenticationChallenge challenge)) { if (challenge.AuthenticationType == AuthenticationType.Negotiate || challenge.AuthenticationType == AuthenticationType.Ntlm) { bool isNewConnection = false; bool needDrain = true; try { if (response.Headers.ConnectionClose.GetValueOrDefault()) { // Server is closing the connection and asking us to authenticate on a new connection. // First, detach the current connection from the pool. This means it will no longer count against the connection limit. // Instead, it will be replaced by the new connection below. connection.DetachFromPool(); connection = await connectionPool.CreateHttp11ConnectionAsync(request, async, cancellationToken).ConfigureAwait(false); connection !.Acquire(); isNewConnection = true; needDrain = false; } if (NetEventSource.Log.IsEnabled()) { NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Uri: {authUri.AbsoluteUri}"); } // Calculate SPN (Service Principal Name) using the host name of the request. // Use the request's 'Host' header if available. Otherwise, use the request uri. // Ignore the 'Host' header if this is proxy authentication since we need to use // the host name of the proxy itself for SPN calculation. string hostName; if (!isProxyAuth && request.HasHeaders && request.Headers.Host != null) { // Use the host name without any normalization. hostName = request.Headers.Host; if (NetEventSource.Log.IsEnabled()) { NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, Host: {hostName}"); } } else { // Need to use FQDN normalized host so that CNAME's are traversed. // Use DNS to do the forward lookup to an A (host) record. // But skip DNS lookup on IP literals. Otherwise, we would end up // doing an unintended reverse DNS lookup. UriHostNameType hnt = authUri.HostNameType; if (hnt == UriHostNameType.IPv6 || hnt == UriHostNameType.IPv4) { hostName = authUri.IdnHost; } else { IPHostEntry result = await Dns.GetHostEntryAsync(authUri.IdnHost, cancellationToken).ConfigureAwait(false); hostName = result.HostName; } if (!isProxyAuth && !authUri.IsDefaultPort && UsePortInSpn) { hostName = string.Create(null, stackalloc char[128], $"{hostName}:{authUri.Port}"); } } string spn = "HTTP/" + hostName; if (NetEventSource.Log.IsEnabled()) { NetEventSource.Info(connection, $"Authentication: {challenge.AuthenticationType}, SPN: {spn}"); } ProtectionLevel requiredProtectionLevel = ProtectionLevel.None; // When connecting to proxy server don't enforce the integrity to avoid // compatibility issues. The assumption is that the proxy server comes // from a trusted source. On macOS we always need to enforce the integrity // to avoid the GSSAPI implementation generating corrupted authentication // tokens. if (!isProxyAuth || OperatingSystem.IsMacOS()) { requiredProtectionLevel = ProtectionLevel.Sign; } NegotiateAuthenticationClientOptions authClientOptions = new NegotiateAuthenticationClientOptions { Package = challenge.SchemeName, Credential = challenge.Credential, TargetName = spn, RequiredProtectionLevel = requiredProtectionLevel, Binding = connection.TransportContext?.GetChannelBinding(ChannelBindingKind.Endpoint) }; using NegotiateAuthentication authContext = new NegotiateAuthentication(authClientOptions); string?challengeData = challenge.ChallengeData; NegotiateAuthenticationStatusCode statusCode; while (true) { string?challengeResponse = authContext.GetOutgoingBlob(challengeData, out statusCode); if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded || challengeResponse == null) { // Response indicated denial even after login, so stop processing and return current response. break; } if (needDrain) { await connection.DrainResponseAsync(response !, cancellationToken).ConfigureAwait(false); } SetRequestAuthenticationHeaderValue(request, new AuthenticationHeaderValue(challenge.SchemeName, challengeResponse), isProxyAuth); response = await InnerSendAsync(request, async, isProxyAuth, connectionPool, connection, cancellationToken).ConfigureAwait(false); if (authContext.IsAuthenticated || !TryGetChallengeDataForScheme(challenge.SchemeName, GetResponseAuthenticationHeaderValues(response, isProxyAuth), out challengeData)) { break; } if (!IsAuthenticationChallenge(response, isProxyAuth)) { // Tail response for Negoatiate on successful authentication. Validate it before we proceed. authContext.GetOutgoingBlob(challengeData, out statusCode); if (statusCode > NegotiateAuthenticationStatusCode.ContinueNeeded) { isNewConnection = false; connection.Dispose(); throw new HttpRequestException(SR.Format(SR.net_http_authvalidationfailure, statusCode), null, HttpStatusCode.Unauthorized); } break; } needDrain = true; } } finally { if (isNewConnection) { connection !.Release(); } } } } return(response !); }
internal static async Task HandleAuthenticationRequest(LoopbackServer.Connection connection, bool useNtlm, bool useNegotiate, bool closeConnection) { HttpRequestData request = await connection.ReadRequestDataAsync(); NegotiateAuthentication authContext = null; string authHeader = null; foreach (HttpHeaderData header in request.Headers) { if (header.Name == "Authorization") { authHeader = header.Value; break; } } if (string.IsNullOrEmpty(authHeader)) { // This is initial request, we reject with showing supported mechanisms. authHeader = string.Empty; if (useNtlm) { authHeader += NtlmAuthHeader + "\r\n"; } if (useNegotiate) { authHeader += NegotiateAuthHeader + "\r\n"; } await connection.SendResponseAsync(HttpStatusCode.Unauthorized, authHeader).ConfigureAwait(false); connection.CompleteRequestProcessing(); // Read next requests and fall-back to loop bellow to process it. request = await connection.ReadRequestDataAsync(); } NegotiateAuthenticationStatusCode statusCode; do { foreach (HttpHeaderData header in request.Headers) { if (header.Name == "Authorization") { authHeader = header.Value; break; } } Assert.NotNull(authHeader); var tokens = authHeader.Split(' ', 2, StringSplitOptions.TrimEntries); // Should be type and base64 encoded blob Assert.Equal(2, tokens.Length); authContext ??= new NegotiateAuthentication(new NegotiateAuthenticationServerOptions { Package = tokens[0] }); byte[]? outBlob = authContext.GetOutgoingBlob(Convert.FromBase64String(tokens[1]), out statusCode); if (outBlob != null && statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded) { authHeader = $"WWW-Authenticate: {tokens[0]} {Convert.ToBase64String(outBlob)}\r\n"; await connection.SendResponseAsync(HttpStatusCode.Unauthorized, authHeader); connection.CompleteRequestProcessing(); request = await connection.ReadRequestDataAsync(); } }while (statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded); if (statusCode == NegotiateAuthenticationStatusCode.Completed) { // If authentication succeeded ask Windows about the identity and send it back as custom header. IIdentity identity = authContext.RemoteIdentity; authHeader = $"{UserHeaderName}: {identity.Name}\r\n"; if (closeConnection) { authHeader += "Connection: close\r\n"; } await connection.SendResponseAsync(HttpStatusCode.OK, authHeader, "foo"); authContext.Dispose(); } else { await connection.SendResponseAsync(HttpStatusCode.Forbidden, "Connection: close\r\n", "boo"); } }