-Is it possible to secure a WCF Data Service by using certificate-based authentication ?
-Is there a resource that describes this process ?
-Can we use Message security with a WCF Data service ?
The answer to all your questions is "yes". Below is a very informative link provided by the Patterns and Practices team at Microsoft to accomplish exactly what you are looking for.
http://msdn.microsoft.com/en-us/library/cc949005.aspx
Certificate based Authentication can be done like this:
Server side:
public class ODataService : DataService<Database>
{
public ODataService()
{
ProcessingPipeline.ProcessingRequest += ProcessingPipeline_ProcessingRequest;
}
void ProcessingPipeline_ProcessingRequest(object sender, DataServiceProcessingPipelineEventArgs e)
{
if (!HttpContext.Current.Request.ClientCertificate.IsPresent)
{
throw new DataServiceException(401, "401 Unauthorized");
}
var cert = new X509Certificate2(HttpContext.Current.Request.ClientCertificate.Certificate);
if (!ValidateCertificate(cert))
{
throw new DataServiceException(401, "401 Unauthorized");
}
var identity = new GenericIdentity(cert.Subject, "ClientCertificate");
var principal = new GenericPrincipal(identity, null);
Thread.CurrentPrincipal = principal;
HttpContext.Current.User = principal;
}
private bool ValidateCertificate(X509Certificate2 cert)
{
// do some validation
}
Client side:
Create a partial class for your database service reference (DataServiceContext)
public partial class Database
{
// ref: http://social.msdn.microsoft.com/Forums/en-US/0aa2a875-fd59-4f3e-a459-9f604b374749/how-do-i-use-certificate-based-authentication-with-data-services-client?forum=adodotnetdataservices
private X509Certificate clientCertificate = null;
public X509Certificate ClientCertificate
{
get
{
return clientCertificate;
}
set
{
if (value == null)
{
// if the event has been hooked up before, we should remove it
if (clientCertificate != null)
{
SendingRequest -= OnSendingRequest_AddCertificate;
}
}
else
{
// hook up the event if its being set to something non-null
if (clientCertificate == null)
{
SendingRequest += OnSendingRequest_AddCertificate;
}
}
clientCertificate = value;
}
}
private void OnSendingRequest_AddCertificate(object sender, SendingRequestEventArgs args)
{
if (null != ClientCertificate)
{
(args.Request as HttpWebRequest).ClientCertificates.Add(ClientCertificate);
}
}
Use it like this
Database db = new Database(new Uri(service));
db.ClientCertificate = CertificateUtil.GetCertificateByThumbprint(StoreName.My,
StoreLocation.LocalMachine,
"<a thumbprint>");
Private key stored on client computer, public key stored on server in Local machine/Trusted Root CA
Remember to require/negotiate client sertificate for this Site in IIS.
(Tested on WCF Data Services 5.2, VS 2012)
Related
I am porting an existing Cloud Service WorkerRole to Service Fabric as a stateless service. The original Cloud Service uses SignalR and Service Bus (as a SignalR backplane), to send notifications out to any client listening. There is a Startup class that does some of the setup:
class Startup
{
public void Configuration(IAppBuilder app)
{
String connectionString = "Endpoint=sb://[name].servicebus.windows.net/;SharedSecretIssuer=owner;SharedSecretValue=[key]";
GlobalHost.DependencyResolver.UseServiceBus(connectionString, "InSys");
app.MapSignalR();
Notifications.Hub = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
}
}
In the OnStart() method in for the WorkerRole I kick-off OWIN with:
var endpoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints["HttpEndpoint"];
var baseUri = $"{endpoint.Protocol}://{endpoint.IPEndpoint}";
var app = WebApp.Start<Startup>(new StartOptions(url: baseUri));
How is this (i.e., connection the to SignalR Service Bus Backplane) done for a stateless service within Service Fabric?
With the help of https://github.com/marcinbudny/SignalRSelfHostScaleOut (which is an example of scaleout using Redis) I think I have this licked.
In the ServiceManifest.xml I added the following EndPoint:
<Endpoint Protocol="http" Name="ServiceEndpoint" Type="Input" Port="8322" />
I also added a Startup class:
public static class Startup
{
public static void ConfigureApp(IAppBuilder app)
{
String connectionString = "Endpoint=sb://[name].servicebus.windows.net/;SharedSecretIssuer=owner;SharedSecretValue=[value]";
GlobalHost.DependencyResolver.UseServiceBus(connectionString, "InSys");
app.MapSignalR();
Notifications.Hub = GlobalHost.ConnectionManager.GetHubContext<InSysMainHub>();
}
}
An OwinCommunicationListener class was also added:
public class OwinCommunicationListener : ICommunicationListener
{
private readonly ServiceEventSource eventSource;
private readonly Action<IAppBuilder> startup;
private readonly ServiceContext serviceContext;
private readonly string endpointName;
private readonly string appRoot;
private IDisposable webApp;
private string publishAddress;
private string listeningAddress;
public OwinCommunicationListener(Action<IAppBuilder> startup, ServiceContext serviceContext, ServiceEventSource eventSource, string endpointName)
: this(startup, serviceContext, eventSource, endpointName, null)
{
}
public OwinCommunicationListener(Action<IAppBuilder> startup, ServiceContext serviceContext, ServiceEventSource eventSource, string endpointName, string appRoot)
{
if (startup == null)
{
throw new ArgumentNullException(nameof(startup));
}
if (serviceContext == null)
{
throw new ArgumentNullException(nameof(serviceContext));
}
if (endpointName == null)
{
throw new ArgumentNullException(nameof(endpointName));
}
if (eventSource == null)
{
throw new ArgumentNullException(nameof(eventSource));
}
this.startup = startup;
this.serviceContext = serviceContext;
this.endpointName = endpointName;
this.eventSource = eventSource;
this.appRoot = appRoot;
}
public Task<string> OpenAsync(CancellationToken cancellationToken)
{
var serviceEndpoint = this.serviceContext.CodePackageActivationContext.GetEndpoint(this.endpointName);
var protocol = serviceEndpoint.Protocol;
int port = serviceEndpoint.Port;
if (this.serviceContext is StatefulServiceContext)
{
StatefulServiceContext statefulServiceContext = (StatefulServiceContext) serviceContext;
listeningAddress = string.Format(
CultureInfo.InvariantCulture,
"{0}://+:{1}/{2}{3}/{4}/{5}",
protocol,
port,
string.IsNullOrWhiteSpace(appRoot)
? string.Empty
: appRoot.TrimEnd('/') + '/',
statefulServiceContext.PartitionId,
statefulServiceContext.ReplicaId,
Guid.NewGuid());
}
else if (serviceContext is StatelessServiceContext)
{
listeningAddress = string.Format(
CultureInfo.InvariantCulture,
"{0}://+:{1}/{2}",
protocol,
port,
string.IsNullOrWhiteSpace(appRoot)
? string.Empty
: appRoot.TrimEnd('/') + '/');
}
else
{
throw new InvalidOperationException();
}
publishAddress = listeningAddress.Replace("+", FabricRuntime.GetNodeContext().IPAddressOrFQDN);
try
{
eventSource.Message("Starting web server on " + listeningAddress);
webApp = WebApp.Start(listeningAddress, appBuilder => startup.Invoke(appBuilder));
eventSource.Message("Listening on " + this.publishAddress);
return Task.FromResult(this.publishAddress);
}
catch (Exception ex)
{
eventSource.Message("Web server failed to open endpoint {0}. {1}", this.endpointName, ex.ToString());
StopWebServer();
throw;
}
}
public Task CloseAsync(CancellationToken cancellationToken)
{
this.eventSource.Message("Closing web server on endpoint {0}", this.endpointName);
this.StopWebServer();
return Task.FromResult(true);
}
public void Abort()
{
this.eventSource.Message("Aborting web server on endpoint {0}", this.endpointName);
this.StopWebServer();
}
private void StopWebServer()
{
if (this.webApp != null)
{
try
{
this.webApp.Dispose();
}
catch (ObjectDisposedException)
{
// no-op
}
}
}
}
And then finally I changed the CreateServiceInstanceListeners method in my stateless service code to:
protected override IEnumerable<ServiceInstanceListener> CreateServiceInstanceListeners()
{
return new[]
{
new ServiceInstanceListener(serviceContext => new OwinCommunicationListener(Startup.ConfigureApp, serviceContext, ServiceEventSource.Current, "ServiceEndpoint"))
};
}
Create a Stateless service With Owin listener. Then in start up configure for signalR and backplane(service bus or sql). The issue ideally you would face is with a negotiate(Hand shake between Signalr client with server) At this point try configure for cross origin request, a sample code for persistent connection will look like below.
Also note the line appBuilder.UseAesDataProtectorProvider("Your Key") as it is important. The result of this is that you wont end up getting HTTP 400 to connect most of the time. This is because SignalR will make at least 2 requests at handshake and those will usually hit two different machines.
Thanks to marcin budny on the explanation.
var config = new HttpConfiguration();
// Configure your origins as required.
var cors = new EnableCorsAttribute("*", "*", "*");
config.EnableCors(cors);
FormatterConfig.ConfigureFormatters(config.Formatters);
RouteConfig.RegisterRoutes(config.Routes);
appBuilder.UseWebApi(config);
GlobalHost.DependencyResolver.UseServiceBus("yourconnection string comes here", "signalrbackplaneserver");
appBuilder.UseAesDataProtectorProvider("some password");
appBuilder.Map("/echo", map =>
{
map.UseCors(CorsOptions.AllowAll).RunSignalR<MyEndPoint>();
});
I am trying to make a .NET client using WSE 3.0 custom policy. I have local wsdl because service uses https with certificate protection. Request soap message needs to be signed and encrypted with certificates one from client for signing, and one from service provider for encryption. I used custom security assertions found on MSDN, here is how my class looks like:
namespace RegistryProxy
{
class CustomSecurityAssertion : SecurityPolicyAssertion
{
TokenProvider<X509SecurityToken> serviceX509TokenProviderValue;
TokenProvider<X509SecurityToken> clientX509TokenProviderValue;
public TokenProvider<X509SecurityToken> ClientX509TokenProvider
{
get
{
return clientX509TokenProviderValue;
}
set
{
clientX509TokenProviderValue = value;
}
}
public TokenProvider<X509SecurityToken> ServiceX509TokenProvider
{
get
{
return serviceX509TokenProviderValue;
}
set
{
serviceX509TokenProviderValue = value;
}
}
public CustomSecurityAssertion()
: base()
{
}
public override SoapFilter CreateClientOutputFilter(FilterCreationContext context)
{
return new CustomSecurityClientOutputFilter(this);
}
public override SoapFilter CreateClientInputFilter(FilterCreationContext context)
{
return new CustomSecurityClientInputFilter(this);
}
public override SoapFilter CreateServiceInputFilter(FilterCreationContext context)
{
return new CustomSecurityServerInputFilter(this);
}
public override SoapFilter CreateServiceOutputFilter(FilterCreationContext context)
{
return new CustomSecurityServerOutputFilter(this);
}
public override void ReadXml(System.Xml.XmlReader reader, IDictionary<string, Type> extensions)
{
if (reader == null)
throw new ArgumentNullException("reader");
if (extensions == null)
throw new ArgumentNullException("extensions");
bool isEmpty = reader.IsEmptyElement;
base.ReadAttributes(reader);
reader.ReadStartElement("CustomSecurityAssertion");
if (!isEmpty)
{
if (reader.MoveToContent() == XmlNodeType.Element && reader.Name == "clientToken")
{
reader.ReadStartElement();
reader.MoveToContent();
// Get the registed security token provider for X.509 certificate security credentials.
Type type = extensions[reader.Name];
object instance = Activator.CreateInstance(type);
if (instance == null)
throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentCulture, "Unable to instantiate policy extension of type {0}.", type.AssemblyQualifiedName));
TokenProvider<X509SecurityToken> clientProvider = instance as TokenProvider<X509SecurityToken>;
// Read the child elements that provide the details about the client's X.509 certificate.
clientProvider.ReadXml(reader, extensions);
this.ClientX509TokenProvider = clientProvider;
reader.ReadEndElement();
}
if (reader.MoveToContent() == XmlNodeType.Element && reader.Name == "serviceToken")
{
reader.ReadStartElement();
reader.MoveToContent();
// Get the registed security token provider for X.509 certificate security credentials.
Type type = extensions[reader.Name];
object instance = Activator.CreateInstance(type);
if (instance == null)
throw new InvalidOperationException(String.Format(System.Globalization.CultureInfo.CurrentCulture, "Unable to instantiate policy extension of type {0}.", type.AssemblyQualifiedName));
TokenProvider<X509SecurityToken> serviceProvider = instance as TokenProvider<X509SecurityToken>;
// Read the child elements that provide the details about the Web service's X.509 certificate.
serviceProvider.ReadXml(reader, extensions);
this.ServiceX509TokenProvider = serviceProvider;
reader.ReadEndElement();
}
base.ReadElements(reader, extensions);
reader.ReadEndElement();
}
}
public override IEnumerable<KeyValuePair<string, Type>> GetExtensions()
{
// Add the CustomSecurityAssertion custom policy assertion to the list of registered
// policy extensions.
List<KeyValuePair<string, Type>> extensions = new List<KeyValuePair<string, Type>>();
extensions.Add(new KeyValuePair<string, Type>("CustomSecurityAssertion", this.GetType()));
if (serviceX509TokenProviderValue != null)
{
// Add any policy extensions that read child elements of the <serviceToken> element
// to the list of registered policy extensions.
IEnumerable<KeyValuePair<string, Type>> innerExtensions = serviceX509TokenProviderValue.GetExtensions();
if (innerExtensions != null)
{
foreach (KeyValuePair<string, Type> extension in innerExtensions)
{
extensions.Add(extension);
}
}
}
if (clientX509TokenProviderValue != null)
{
// Add any policy extensions that read child elements of the <clientToken> element
// to the list of registered policy extensions.
IEnumerable<KeyValuePair<string, Type>> innerExtensions = clientX509TokenProviderValue.GetExtensions();
if (innerExtensions != null)
{
foreach (KeyValuePair<string, Type> extension in innerExtensions)
{
extensions.Add(extension);
}
}
}
return extensions;
}
// </snippet16
}
class RequestState
{
SecurityToken clientToken;
SecurityToken serverToken;
public RequestState(SecurityToken cToken, SecurityToken sToken)
{
clientToken = cToken;
serverToken = sToken;
}
public SecurityToken ClientToken
{
get { return clientToken; }
}
public SecurityToken ServerToken
{
get { return serverToken; }
}
}
class CustomSecurityServerInputFilter : ReceiveSecurityFilter
{
public CustomSecurityServerInputFilter(CustomSecurityAssertion parentAssertion)
: base(parentAssertion.ServiceActor, false)
{
}
public override void ValidateMessageSecurity(SoapEnvelope envelope, Security security)
{
SecurityToken clientToken = null;
SecurityToken serverToken = null;
// Ensure incoming SOAP messages are signed and encrypted.
foreach (ISecurityElement elem in security.Elements)
{
if (elem is MessageSignature)
{
MessageSignature sig = (MessageSignature)elem;
clientToken = sig.SigningToken;
}
if (elem is EncryptedData)
{
EncryptedData enc = (EncryptedData)elem;
serverToken = enc.SecurityToken;
}
}
if (clientToken == null || serverToken == null)
throw new Exception("Incoming message did not meet security requirements");
RequestState state = new RequestState(clientToken, serverToken);
envelope.Context.OperationState.Set(state);
}
}
class CustomSecurityServerOutputFilter : SendSecurityFilter
{
public CustomSecurityServerOutputFilter(CustomSecurityAssertion parentAssertion)
: base(parentAssertion.ServiceActor, false)
{
}
public override void SecureMessage(SoapEnvelope envelope, Security security)
{
RequestState state = envelope.Context.OperationState.Get<RequestState>();
// Sign the message with the Web service's security token.
security.Tokens.Add(state.ServerToken);
security.Elements.Add(new MessageSignature(state.ServerToken));
// Encrypt the message with the client's security token.
security.Elements.Add(new EncryptedData(state.ClientToken));
}
}
class CustomSecurityClientInputFilter : ReceiveSecurityFilter
{
public CustomSecurityClientInputFilter(CustomSecurityAssertion parentAssertion)
: base(parentAssertion.ServiceActor, true)
{
}
public override void ValidateMessageSecurity(SoapEnvelope envelope, Security security)
{
RequestState state;
bool signed = false;
bool encrypted = false;
// Get the request state out of the operation state.
state = envelope.Context.OperationState.Get<RequestState>();
// Make sure the message was signed with the server's security token.
foreach (ISecurityElement elem in security.Elements)
{
if (elem is MessageSignature)
{
MessageSignature sig = (MessageSignature)elem;
if (sig.SigningToken.Equals(state.ServerToken))
signed = true;
}
if (elem is EncryptedData)
{
EncryptedData enc = (EncryptedData)elem;
if (enc.SecurityToken.Equals(state.ClientToken))
encrypted = true;
}
}
if (!signed || !encrypted)
throw new Exception("Response message does not meet security requirements");
}
}
class CustomSecurityClientOutputFilter : SendSecurityFilter
{
SecurityToken clientToken;
SecurityToken serverToken;
public CustomSecurityClientOutputFilter(CustomSecurityAssertion parentAssertion)
: base(parentAssertion.ServiceActor, true)
{
ISecurityTokenManager stm = SecurityTokenManager.GetSecurityTokenManagerByTokenType(WSTrust.TokenTypes.X509v3);
X509SecurityTokenManager x509tm = stm as X509SecurityTokenManager;
x509tm.DefaultSessionKeyAlgorithm = "AES128";
x509tm.DefaultKeyAlgorithm = "RSA15";
// Get the client security token.
clientToken = X509TokenProvider.CreateToken(StoreLocation.LocalMachine, StoreName.My, "serial removed", X509FindType.FindBySerialNumber);
// Get the server security token.
serverToken = X509TokenProvider.CreateToken(StoreLocation.LocalMachine, StoreName.My, "serial removed",X509FindType.FindBySerialNumber);
}
public override void SecureMessage(SoapEnvelope envelope, Security security)
{
ISecurityTokenManager stm = X509SecurityTokenManager.GetSecurityTokenManagerByTokenType(WSTrust.TokenTypes.X509v3);
X509SecurityTokenManager x509tm = stm as X509SecurityTokenManager;
x509tm.DefaultSessionKeyAlgorithm = "AES128";
x509tm.DefaultKeyAlgorithm = "RSA15";
// Sign the SOAP message with the client's security token.
security.Tokens.Add(clientToken);
security.Elements.Add(new MessageSignature(clientToken));
// Encrypt the SOAP message with the client's security token.
security.Elements.Add(new EncryptedData(serverToken));
// Encrypt the client's security token with the server's security token.
security.Elements.Add(new EncryptedData(serverToken, "#" + clientToken.Id));
// Store the client and server security tokens in the request state.
RequestState state = new RequestState(clientToken, serverToken);
// Store the request state in the proxy's operation state.
// This makes these tokens accessible when SOAP responses are
// verified to have sufficient security requirements.
envelope.Context.OperationState.Set(state);
}
}
}
After class in set into project I made a wse3policyCache.config to use certificates:
<policies xmlns="http://schemas.microsoft.com/wse/2005/06/policy">
<extensions>
<extension name="CustomSecurityAssertion" type="GiftRegistryProxy.CustomSecurityAssertion,GiftRegistryProxy" />
</extensions>
<policy name="ClientPolicy">
<CustomSecurityAssertion>
<clientToken>
<x509
storeLocation="LocalMachine"
storeName="My"
findValue="serial number"
findType="FindBySerialNumber" />
</clientToken>
<serviceToken>
<x509
storeLocation="LocalMachine"
storeName="My"
findValue="serial number"
findType="FindBySerialNumber" />
</serviceToken>
</CustomSecurityAssertion>
</policy>
</policies>
And when everything is set I run my client, my client returns message:
The request was aborted: Could not create SSL/TLS secure channel.
When I look Network monitor it says
TLS:TLS Rec Layer-1 HandShake: Client Hello.
TLS:TLS Rec Layer-1 HandShake: Server Hello. Certificate. Certificate.
TLS:TLS Rec Layer-1 HandShake: Certificate. Client Key Exchange.; TLS Rec Layer-2 Cipher Change Spec; TLS Rec Layer-3 HandShake: Encrypted Handshake Message.
TLS:TLS Rec Layer-1 Encrypted Alert --Problem
Can anyone explain me why this happens and type here if possible?
I have the following scenario going on:
A windows "fat client" application is connecting to a WCF webservice. Both, client and webservice use exact the same binding, which looks like this:
private static NetTcpBinding Message_Security_UserName_Credentials()
{
NetTcpBinding binding = new NetTcpBinding();
binding.Security.Mode = SecurityMode.Message;
binding.Security.Message.ClientCredentialType = MessageCredentialType.UserName;
binding.Security.Transport.ProtectionLevel = System.Net.Security.ProtectionLevel.EncryptAndSign;
binding.PortSharingEnabled = true;
return binding;
}
The client sends "custom" client credentials to the webservice. The custom client credential class is this:
public class CustomClientCredentials : ClientCredentials
{
public CustomClientCredentials()
{
AuthorizationToken = String.Empty;
this.ClientCertificate.Certificate = Certificates.ClientPFX;
this.ServiceCertificate.Authentication.CertificateValidationMode = System.ServiceModel.Security.X509CertificateValidationMode.Custom;
this.ServiceCertificate.Authentication.CustomCertificateValidator = new CustomClientX509CertificateValidator("CN");
}
private string authorizationtoken;
public string AuthorizationToken
{
get
{
return this.authorizationtoken;
}
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
this.authorizationtoken = value;
}
}
public String Name
{
set
{
this.UserName.UserName = value;
}
}
public String Password
{
set
{
this.UserName.Password = value;
}
}
protected CustomClientCredentials(CustomClientCredentials other)
: base(other)
{
this.AuthorizationToken = other.AuthorizationToken;
}
protected override ClientCredentials CloneCore()
{
return new CustomClientCredentials(this);
}
}
In short, the process of sending the custom client credentials to the service looks like this:
ChannelFactory<ILoginService> factory = new ChannelFactory<ILoginService> (binding, endpointaddress);
factory.Endpoint.Behaviors.Remove<ClientCredentials>();
CustomClientCredentials credentials = new CustomClientCredentials() {Name = this.User.EMail, Password = this.User.Password, AuthorizationToken = String.Empty};
factory.Endpoint.Behaviors.Add(credentials);
ILoginService client = factory.CreateChannel();
Token result = client.LogIn();
On the server, I use a custom UserPasswordValidator to read out the client credentials. It looks like this:
public class CustomServiceUserNamePasswordValidator : System.IdentityModel.Selectors.UserNamePasswordValidator
{
public override void Validate(string userName, string password)
{
if (null == userName || null == password)
{
throw new ArgumentNullException();
}
}
}
Up to this point everything works fine. As you can see in my custom ClientCredentials class, I want to send more additional information to the server.
My question is: What must I do, to read out the received custom client credentials on the server?
The theory in my head is, that I simply must tell the service endpoint on the server, that he should expect a certain type of credentials and then he can evaluate them.
Validating custom client credentials may not an easy tasks but you can following this link for validation. I would suggest also to follow this link for custom credential implementation.
I'm trying to access a WebService that requires a certificate issued by themselves, and I'm getting errors during the validation.
BasicHttpsBinding _binding;
EndpointAddress _address;
X509Certificate2 _certificate;
public ServiceAccess() //Constructor
{
_binding = new BasicHttpsBinding(BasicHttpsSecurityMode.TransportWithCredential);
//With Credential because I've read on many tutorials that if I don't use this mode,
//IIS will validate the certificate first and fail without ever calling my custom validator
_certificate = new X509Certificate2(); //I don't know why 2 except for the identity code below
Byte[] bytes = Loader.EmbeddedResources.ToByteArray("Namespace.Project.certificate.crt");
_certificate.Import(bytes);
EndpointIdentity identity = EndpointIdentity.Create509CertificateIdentity(_certificate);
_address = new EndpointAddress(new Uri("https://www.service.com/secure"), identity);
}
public void Start()
{
ServiceClient client = new ServiceClient(_binding, _address);
client.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.Custom;
client.ClientCredentials.ServiceCertificate.Authentication.CustomCertificateValidator = new CustomCertificateValidator();
client.ClientCredentials.ClientCertificate.Certificate = _certificate; //why do I need to set this certificate again if I already defined it in the endpointAddress?
try
{
client.DoSomething();
}
catch (Exception ex)
{
//Error estabilishing a secure connection
//Error the certificate doesn't have a private key
}
}
And my Custom Validator:
public class CustomValidator : X509CertificateValidator
{
public override void Validate(X509Certificate2 certificate)
{
//This never gets called
throw new Exception("Custom Validator Called!");
}
}
Since the certificate was issued by themselves, I'd like to add the custom validator to debug and make it skip whatever's causing the validation errors, but keep everything else as is.
How can I make this work?
I have an existing, working ASP.NET MVC 4 web application. I have written my own RoleProvider and I am using the standard [Authorize] attribute. My controllers look like this:
[Authorize(Roles="ContactAdmins")] //System.Web.Mvc
public ActionResult Index()
I would like to add a WebAPI controller to my application, and take advantage of my existing plumbing
[Authorize(Roles="ContactAdmins")] //System.Web.Http
public IEnumerable<Contact> Get()
This works for Javascript ajax calls from within my site (since the browser user is already authenticated with a Forms auth cookie). My question is from a C# Console Application (or any other application that is not part of my web app) how can I authenticate to this API?
Lets assume that for the parts of my API which are public, I am using code very similar to what is found at this question Consuming WebApi in MVC3.
var url = "http://localhost:9000/api/contacts";
using (var client = new WebClient())
using (var reader = XmlReader.Create(client.OpenRead(url)))
{
var serializer = new XmlSerializer(typeof(Contact[]));
var contacts = (Contact[])serializer.Deserialize(reader);
// TODO: Do something with the contacts
}
What would I need to modify here? Or would I have to scrap this and use a completely different approach? I am not tied to using Forms for API Authentication of remote clients, but I would like to keep the current elegant approach for JavaScript clients that are part of the app (just request API since forms cookie is set).
You could combine the standard Forms Auth with a custom Basic Auth, based on the same primitives than Forms Auth. Note with Basic, HTTPS is strongly recommended (and in fact more and more Windows component do not support Basic+HTTP by default nowadays).
Here is a sample code for a Basic Authentication Module that reuses code from Forms Auth. It also comes with it's own configuration section (named 'basicAuth'). You want to make sure both auths (Forms and Basic) use the same cookie and parameters when configured together:
using System;
using System.ComponentModel;
using System.Configuration;
using System.Globalization;
using System.Net;
using System.Security.Principal;
using System.Text;
using System.Web;
using System.Web.Configuration;
using System.Web.Security;
namespace MySecurity
{
public class BasicAuthenticationModule : IHttpModule
{
public event EventHandler<BasicAuthenticationEventArgs> Authenticate;
public void Dispose()
{
}
protected virtual string GetRealm(HttpContext context)
{
return BasicAuthenticationSection.Current.GetRealm(context);
}
public virtual void Init(HttpApplication context)
{
context.AuthenticateRequest += OnAuthenticateRequest;
context.EndRequest += OnEndRequest;
}
protected virtual bool FormsAuthenticate(HttpContext context, string login, string password, string realm)
{
// check ad-hoc forms credentials, as we can support it even if forms auth is not configured
FormsAuthenticationConfiguration c = ((AuthenticationSection)ConfigurationManager.GetSection("system.web/authentication")).Forms;
if ((c.Credentials == null) || (c.Credentials.Users == null))
return false;
foreach (FormsAuthenticationUser user in c.Credentials.Users)
{
if ((string.Compare(user.Name, login, true, CultureInfo.CurrentCulture) == 0) &&
(string.Compare(user.Password, password, true, CultureInfo.CurrentCulture) == 0))
return true;
}
return false;
}
protected virtual bool OnAuthenticate(HttpContext context, string login, string password, string realm)
{
EventHandler<BasicAuthenticationEventArgs> handler = Authenticate;
if (handler != null)
{
BasicAuthenticationEventArgs e = new BasicAuthenticationEventArgs(context, login, password, realm);
handler(this, e);
return !e.Cancel;
}
return FormsAuthenticate(context, login, password, realm);
}
protected virtual string[] GetUserRoles(HttpContext context, string login, string realm)
{
// TODO: overwrite if needed
return new string[0];
}
protected virtual IPrincipal GetUser(HttpContext context, FormsAuthenticationTicket ticket)
{
return new GenericPrincipal(new BasicAuthenticationIdentity(ticket), GetUserRoles(context, ticket.Name, GetRealm(context)));
}
protected virtual void OnAuthenticated(HttpContext context)
{
}
protected virtual void OnEndRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
if (application.Response.StatusCode != (int)HttpStatusCode.Unauthorized)
return;
string basic = "Basic Realm=\"" + GetRealm(application.Context) + "\"";
application.Response.AppendHeader("WWW-Authenticate", basic);
}
public static void SignOut()
{
if (HttpContext.Current == null)
return;
HttpContext.Current.Request.Cookies.Remove(BasicAuthenticationSection.Current.Name);
HttpContext.Current.Response.Cookies.Remove(BasicAuthenticationSection.Current.Name);
HttpCookie cookie = new HttpCookie(BasicAuthenticationSection.Current.Name);
cookie.Expires = DateTime.Now.AddDays(-1);
HttpContext.Current.Response.Cookies.Add(cookie);
}
public static bool IsAuthenticated(HttpContext context)
{
if ((context == null) || (context.User == null) || (context.User.Identity == null))
return false;
return context.User.Identity.IsAuthenticated;
}
protected virtual void OnAuthenticateRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
if ((IsAuthenticated(application.Context)) && (!BasicAuthenticationSection.Current.ReAuthenticate))
return;
string encryptedTicket;
FormsAuthenticationTicket ticket;
HttpCookie cookie = application.Context.Request.Cookies[BasicAuthenticationSection.Current.Name];
if (cookie == null)
{
// no cookie, check auth header
string authHeader = application.Context.Request.Headers["Authorization"];
if ((string.IsNullOrEmpty(authHeader)) || (!authHeader.StartsWith("Basic ", StringComparison.InvariantCultureIgnoreCase)))
{
ResponseAccessDenied(application);
return;
}
string login;
string password;
string lp = authHeader.Substring(6).Trim();
if (string.IsNullOrEmpty(lp))
{
ResponseAccessDenied(application);
return;
}
lp = Encoding.Default.GetString(Convert.FromBase64String(lp));
if (string.IsNullOrEmpty(lp.Trim()))
{
ResponseAccessDenied(application);
return;
}
int pos = lp.IndexOf(':');
if (pos < 0)
{
login = lp;
password = string.Empty;
}
else
{
login = lp.Substring(0, pos).Trim();
password = lp.Substring(pos + 1).Trim();
}
if (!OnAuthenticate(application.Context, login, password, GetRealm(application.Context)))
{
ResponseAccessDenied(application);
return;
}
// send cookie back to client
ticket = new FormsAuthenticationTicket(login, false, (int)BasicAuthenticationSection.Current.Timeout.TotalMinutes);
encryptedTicket = FormsAuthentication.Encrypt(ticket);
cookie = new HttpCookie(BasicAuthenticationSection.Current.Name, encryptedTicket);
application.Context.Response.Cookies.Add(cookie);
// don't overwrite context user if it's been set
if ((!IsAuthenticated(application.Context)) || (BasicAuthenticationSection.Current.ReAuthenticate))
{
application.Context.User = GetUser(application.Context, ticket);
}
OnAuthenticated(application.Context);
application.Context.Response.StatusCode = (int)HttpStatusCode.OK;
return;
}
// there is a cookie, check it
encryptedTicket = cookie.Value;
if (string.IsNullOrEmpty(encryptedTicket))
{
ResponseAccessDenied(application);
return;
}
try
{
ticket = FormsAuthentication.Decrypt(encryptedTicket);
}
catch
{
ResponseAccessDenied(application);
return;
}
if (ticket.Expired)
{
ResponseAccessDenied(application);
return;
}
// set context user
// don't overwrite context user if it's been set
if ((!IsAuthenticated(application.Context) || (BasicAuthenticationSection.Current.ReAuthenticate)))
{
application.Context.User = GetUser(application.Context, ticket);
}
OnAuthenticated(application.Context);
}
protected virtual void WriteAccessDenied(HttpApplication application)
{
if (application == null)
throw new ArgumentNullException("application");
application.Context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
application.Context.Response.StatusDescription = "Unauthorized";
application.Context.Response.Write(application.Context.Response.StatusCode + " " + application.Context.Response.StatusDescription);
}
protected virtual void ResponseAccessDenied(HttpApplication application)
{
// if there is a bad cookie, kill it
application.Context.Request.Cookies.Remove(BasicAuthenticationSection.Current.Name);
application.Context.Response.Cookies.Remove(BasicAuthenticationSection.Current.Name);
HttpCookie cookie = new HttpCookie(BasicAuthenticationSection.Current.Name);
cookie.Expires = DateTime.Now.AddDays(-1);
HttpContext.Current.Response.Cookies.Add(cookie);
WriteAccessDenied(application);
application.CompleteRequest();
}
}
public class BasicAuthenticationSection : ConfigurationSection
{
public const string SectionName = "basicAuth";
private const string DefaultCookieName = "." + SectionName;
private static BasicAuthenticationSection _current;
public static BasicAuthenticationSection Current
{
get
{
return _current ?? (_current = ConfigurationManager.GetSection(SectionName) as BasicAuthenticationSection ?? new BasicAuthenticationSection());
}
}
[StringValidator(MinLength = 1), ConfigurationProperty("name", DefaultValue = DefaultCookieName)]
public string Name
{
get
{
return (string)base["name"];
}
}
internal string GetRealm(HttpContext context)
{
if (!string.IsNullOrEmpty(Realm))
return Realm;
return context.Request.Url.Host;
}
[ConfigurationProperty("realm", DefaultValue = "")]
public string Realm
{
get
{
return (string)base["realm"];
}
}
[ConfigurationProperty("domain", DefaultValue = "")]
public string Domain
{
get
{
return (string)base["domain"];
}
}
[ConfigurationProperty("reAuthenticate", DefaultValue = false)]
public bool ReAuthenticate
{
get
{
return (bool)base["reAuthenticate"];
}
}
[TypeConverter(typeof(TimeSpanMinutesConverter)), ConfigurationProperty("timeout", DefaultValue = "30"), PositiveTimeSpanValidator]
public TimeSpan Timeout
{
get
{
return (TimeSpan)base["timeout"];
}
}
}
public class BasicAuthenticationIdentity : IIdentity
{
public BasicAuthenticationIdentity(FormsAuthenticationTicket ticket)
{
if (ticket == null)
throw new ArgumentNullException("ticket");
Ticket = ticket;
}
public FormsAuthenticationTicket Ticket;
public string AuthenticationType
{
get
{
return BasicAuthenticationSection.SectionName;
}
}
public bool IsAuthenticated
{
get
{
return true;
}
}
public string Name
{
get
{
return Ticket.Name;
}
}
}
public class BasicAuthenticationEventArgs : CancelEventArgs
{
public BasicAuthenticationEventArgs(HttpContext context, string login, string password, string realm)
{
if (context == null)
throw new ArgumentNullException("context");
Context = context;
Login = login;
Password = password;
Realm = realm;
}
public HttpContext Context { get; private set; }
public string Realm { get; private set; }
public string Login { get; private set; }
public string Password { get; private set; }
public IPrincipal User { get; set; }
}
}
Once this is installed server side, you can configure the WebClient to use Basic auth:
WebClient client = new WebClient();
client.Credentials = new NetworkCredential("username", "password");
There are numerous ways to share the cookie with the console application. Take a look at some ideas here:
http://netpl.blogspot.com/2008/02/clickonce-webservice-and-shared-forms.html
Another simple option would be to expose a web method which doesn't require any authentication, gets the username and password and returns the cookie to the client.
No matter what approach you take, your goal is to somehow get the forms cookie at the console application side. From there you are easily done as all you do is you attach the cookie to your requests. The web api will accept the cookie happily.