Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add limited support for SpnEndpointIdentity with HTTP #4385

Merged
merged 1 commit into from
Oct 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/System.Private.ServiceModel/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -2481,9 +2481,6 @@
<data name="SignedSupportingTokenNotExpected" xml:space="preserve">
<value>A signed supporting token is not expected in the security header in this context.</value>
</data>
<data name="String1" xml:space="preserve">
<value>Crypto algorithm '{0}' not supported in this context.</value>
</data>
<data name="TokenManagerCouldNotReadToken" xml:space="preserve">
<value>Security token manager could not parse token with name '{0}', namespace '{1}', valueType '{2}'.</value>
</data>
Expand Down Expand Up @@ -2820,4 +2817,13 @@
<data name="TheBindingForDoesnTSupportOrderedDelivery1" xml:space="preserve">
<value>The DeliveryRequirementsAttribute on contract '{0}' specifies a QueuedDeliveryRequirements value of NotAllowed. However, the configured binding for this contract specifies that it does support queued delivery. A queued binding may not be used with this contract.</value>
</data>
</root>
<data name="HttpsExplicitIdentity" xml:space="preserve">
<value>The HTTPS channel factory does not support explicit specification of an identity in the EndpointAddress unless the authentication scheme is NTLM or Negotiate.</value>
</data>
<data name="HttpsIdentityMultipleCerts" xml:space="preserve">
<value>The endpoint identity specified when creating the HTTPS channel to '{0}' contains multiple server certificates. However, the HTTPS transport only supports the specification of a single server certificate. In order to create an HTTPS channel, please specify no more than one server certificate in the endpoint identity.</value>
</data>
<data name="OnlyDefaultSpnServiceSupported" xml:space="preserve">
<value>Only HOST and HTTP service principal names are supported .</value>
</data>
</root>
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ internal class ClientWebSocketTransportDuplexSessionChannel : WebSocketTransport
private SecurityTokenProviderContainer _webRequestTokenProvider;
private SecurityTokenProviderContainer _webRequestProxyTokenProvider;
private volatile bool _cleanupStarted;
private volatile bool _cleanupIdentity;

public ClientWebSocketTransportDuplexSessionChannel(HttpChannelFactory<IDuplexSessionChannel> channelFactory, ClientWebSocketFactory connectionFactory, EndpointAddress remoteAddress, Uri via)
: base(channelFactory, remoteAddress, via)
Expand Down Expand Up @@ -67,14 +66,6 @@ protected internal override async Task OnOpenAsync(TimeSpan timeout)

ChannelParameterCollection channelParameterCollection = new ChannelParameterCollection();

if (HttpChannelFactory<IDuplexSessionChannel>.MapIdentity(RemoteAddress, _channelFactory.AuthenticationScheme))
{
lock (ThisLock)
{
_cleanupIdentity = HttpTransportSecurityHelpers.AddIdentityMapping(Via, RemoteAddress);
}
}

X509Certificate2 clientCertificate = null;
HttpsChannelFactory<IDuplexSessionChannel> httpsChannelFactory = _channelFactory as HttpsChannelFactory<IDuplexSessionChannel>;
if (httpsChannelFactory != null && httpsChannelFactory.RequireClientCertificate)
Expand Down Expand Up @@ -207,6 +198,10 @@ private async Task<WebSocket> CreateWebSocketWithFactoryAsync(X509Certificate2 c
{
headers[WebSocketTransportSettings.BinaryEncoderTransferModeHeader] = _channelFactory.TransferMode.ToString();
}
if (HttpChannelFactory<IDuplexSessionChannel>.MapIdentity(RemoteAddress, _channelFactory.AuthenticationScheme))
{
headers[HttpRequestHeader.Host] = HttpTransportSecurityHelpers.GeIdentityHostHeader(RemoteAddress);
}

var credentials = _channelFactory.GetCredentials();
ws = await _connectionFactory.CreateWebSocketAsync(Via, headers, credentials, WebSocketSettings.Clone(), timeoutHelper);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,11 @@ public HttpClientChannelAsyncRequest(HttpClientRequestChannel channel)
public async Task SendRequestAsync(Message message, TimeoutHelper timeoutHelper)
{
_timeoutHelper = timeoutHelper;
if (_channel.Factory.MapIdentity(_to))
{
HttpTransportSecurityHelpers.AddIdentityMapping(_to, message);
}

_factory.ApplyManualAddressing(ref _to, ref _via, message);
_httpClient = await _channel.GetHttpClientAsync(_to, _via, _timeoutHelper);

Expand Down Expand Up @@ -1202,10 +1207,6 @@ private bool PrepareMessageHeaders(Message message)
_httpRequestMessage.Headers.Expect.TryParseAdd(value);
}
}
else if (string.Compare(name, "host", StringComparison.OrdinalIgnoreCase) == 0)
{
// this should be controlled through Via
}
else if (string.Compare(name, "referer", StringComparison.OrdinalIgnoreCase) == 0)
{
// referrer is proper spelling, but referer is the what is in the protocol.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.


using System.IdentityModel.Claims;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.Net.Http;
Expand Down Expand Up @@ -59,6 +60,32 @@ public override T GetProperty<T>()

protected override void ValidateCreateChannelParameters(EndpointAddress remoteAddress, Uri via)
{
if (remoteAddress.Identity != null)
{
X509CertificateEndpointIdentity certificateIdentity =
remoteAddress.Identity as X509CertificateEndpointIdentity;
if (certificateIdentity != null)
{
if (certificateIdentity.Certificates.Count > 1)
{
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(nameof(remoteAddress), SR.Format(
SR.HttpsIdentityMultipleCerts, remoteAddress.Uri));
}
}

EndpointIdentity identity = remoteAddress.Identity;
bool validIdentity = (certificateIdentity != null)
|| ClaimTypes.Spn.Equals(identity.IdentityClaim.ClaimType)
|| ClaimTypes.Upn.Equals(identity.IdentityClaim.ClaimType)
|| ClaimTypes.Dns.Equals(identity.IdentityClaim.ClaimType);

if (!IsWindowsAuth(AuthenticationScheme)
&& !validIdentity)
{
throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(nameof(remoteAddress), SR.HttpsExplicitIdentity);
}
}

if (string.Compare(via.Scheme, "wss", StringComparison.OrdinalIgnoreCase) != 0)
{
ValidateScheme(via);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using System.ServiceModel.Diagnostics;
using System.ServiceModel.Security;
using System.ServiceModel.Security.Tokens;
using System.Text;
Expand Down Expand Up @@ -328,17 +329,42 @@ internal static class HttpTransportSecurityHelpers
{
private static Dictionary<string, int> s_targetNameCounter = new Dictionary<string, int>();

public static bool AddIdentityMapping(Uri via, EndpointAddress target)
public static void AddIdentityMapping(EndpointAddress target, Message message)
{
// On Desktop, we do mutual auth when the EndpointAddress has an identity. We need
// support from HttpClient before any functionality can be added here.
return false;
var hostHeader = GeIdentityHostHeader(target);
HttpRequestMessageProperty requestProperty;
if (!message.Properties.TryGetValue(HttpRequestMessageProperty.Name, out requestProperty))
{
requestProperty = new HttpRequestMessageProperty();
message.Properties.Add(HttpRequestMessageProperty.Name, requestProperty);
}

requestProperty.Headers[HttpRequestHeader.Host] = hostHeader;
}

public static void RemoveIdentityMapping(Uri via, EndpointAddress target, bool validateState)
public static string GeIdentityHostHeader(EndpointAddress target)
{
// On Desktop, we do mutual auth when the EndpointAddress has an identity. We need
// support from HttpClient before any functionality can be added here.
EndpointIdentity identity = target.Identity;
string value;
if (identity != null && !(identity is X509CertificateEndpointIdentity))
{
value = SecurityUtils.GetSpnFromIdentity(identity, target);
}
else
{
value = SecurityUtils.GetSpnFromTarget(target);
}

// HttpClientHandler supports specifying the SPN via the HOST header. The service name is hard coded to "HTTP/". "HTTP/"
// is an alias for the "HOST/" service name so we accept either but can't accept anything else.
if (!(value.StartsWith("host/", StringComparison.OrdinalIgnoreCase) || value.StartsWith("http/", StringComparison.OrdinalIgnoreCase)))
{
throw Fx.Exception.AsError(new InvalidOperationException(SR.OnlyDefaultSpnServiceSupported));
}

// The leading service name has been constrained to be either "HTTP/" or "HOST/" which are both 5 charactes long.
// This needs to be removed to provide just the hostname part for the Host header.
return value.Substring(5);
}

public static void AddServerCertIdentityValidation(HttpClientHandler httpClientHandler, EndpointAddress to)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.ServiceModel;
using System.Text;
using System.Threading.Channels;
using Infrastructure.Common;
using Xunit;

public static class HttpIdentityTests
{
[WcfFact]
public static void SpnEndpointIdentity_NotSupportedThrows()
{
var identity = EndpointIdentity.CreateSpnIdentity("SERVICE/serverhostname");
var endpointAddress = new EndpointAddress(new Uri("https://serverhostname/fakeService.svc"), identity);
var binding = new BasicHttpsBinding(BasicHttpsSecurityMode.Transport);
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows;
var factory = new ChannelFactory<IService>(binding, endpointAddress);
var channel = factory.CreateChannel();
Assert.Throws<InvalidOperationException>(() => channel.Echo(""));
}

[WcfFact]
public static void UpnEndpointIdentityThrows()
{
var identity = EndpointIdentity.CreateUpnIdentity("user@contoso.com");
var endpointAddress = new EndpointAddress(new Uri("https://serverhostname/fakeService.svc"), identity);
var binding = new BasicHttpsBinding(BasicHttpsSecurityMode.Transport);
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Windows;
var factory = new ChannelFactory<IService>(binding, endpointAddress);
var channel = factory.CreateChannel();
Assert.Throws<InvalidOperationException>(() => channel.Echo(""));
}

[ServiceContract]
public interface IService
{
[OperationContract]
string Echo(string value);
}
}