Skip to content

Commit

Permalink
Add limited support for SpnEndpointIdentity with HTTP (#4385)
Browse files Browse the repository at this point in the history
Co-authored-by: Matt Connew <matt.connew@microsoft.com>
  • Loading branch information
mconnew and Matt Connew authored Oct 12, 2020
1 parent d71557a commit 72a60fa
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 24 deletions.
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);
}
}

0 comments on commit 72a60fa

Please sign in to comment.