How can I evaluate custom ClientCredentials on the server in WCF? - c#

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.

Related

How to use OData Authentication (OData Client V7)?

How can I use Kerberos / NTLM authentication (like in the HttpClient) within the OData Client from Microsoft (Microsoft.OData.Client)?
I am using the package Microsoft.OData.Client 7.9.0 and I am trying to connect to a OData endpoint with https and authentication enabled. However I am not able to retrieve any data, instead this exception is thrown:
Microsoft.OData.Edm.Csdl.EdmParseException: "Encountered the following errors when parsing the CSDL document:
XmlError : Root element is missing. : (0, 0)"
It seems that the context could not find the requested resource because of a lack of permissions. This is the referencial implementation:
// Simple data class
public class Person
{
public string Id { get; set; }
public string UserName { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
// OData service context
public class Container : DataServiceContext
{
public DataServiceQuery<Person> People { get; }
public Container(Uri serviceRoot) : base(serviceRoot)
{
People = base.CreateQuery<Person>(nameof(People));
// This is not working ...
Credentials = new NetworkCredential("user", #"p#ssw0rd!");
}
}
Container context = new Container(new Uri("https://targetservice.dev/ODataV4/$metadata"));
var result = context.People.Execute() as QueryOperationResponse<Person>;
Providing credentials to the Credentials property does not seem to have any effect here.
Set the credentials from the outside, before using the container for the first time. After all, you don't want to modify your Container every time the credentials change. Nor do you want to hard-code into the Container the mechanism you use to retrieve those credentials.
Assuming both the server and client are in the same domain, one can connect using Windows authentication as the current user, with CredentialCache.DefaultNetworkCredentials
var uri=new Uri("https://targetservice.dev/ODataV4/$metadata");
Container context = new Container(uri){
Credentials = CredentialCache.DefaultNetworkCredentials
};
var result = context.People.Execute() as QueryOperationResponse<Person>;
If you want to connect from a non-Domain machine or use a different account, you'll have to create a NetworkCredential instance :
var credential = new NetworkCredential("MyDomain","UserName","Password");
Container context = new Container(uri){
Credentials = credential
};
The main advantage of Windows authentication is that you don't need to explicitly specify the credentials. Any remote calls will be made using the current account. This way there's no password to store, change or leak. This of course assumes that both the client and server are in the same Windows domain, otherwise the client account won't be recognized by the server.
Wrong URL
The exception snippet complains about the server's response, not failed authentication. XmlError : Root element is missing. : (0, 0) means that the response was not an XML document. It's not an error response either. If the server had responded with a 4xx or 5xx status, a different exception would be thrown.
The service URL is wrong and shouldn't contain the $metadata suffix. As the constructor name says DataServiceContext(Uri serviceRoot) the URL should be the service's root. That URL is stored in the BaseUriResolver property:
internal DataServiceContext(Uri serviceRoot, ODataProtocolVersion maxProtocolVersion, ClientEdmModel model)
{
Debug.Assert(model != null, "model != null");
this.model = model;
this.baseUriResolver = UriResolver.CreateFromBaseUri(serviceRoot, ServiceRootParameterName);
The metadata URL is created by the GetMetadataUri method whose code is:
public virtual Uri GetMetadataUri()
{
// TODO: resolve the location of the metadata endpoint for the service by using an HTTP OPTIONS request
Uri metadataUri = UriUtil.CreateUri(UriUtil.UriToString(
this.BaseUriResolver.GetBaseUriWithSlash()) +
XmlConstants.UriMetadataSegment,
UriKind.Absolute);
return metadataUri;
}
Using https://targetservice.dev/ODataV4/$metadata as the service URL would result in an invalid https://targetservice.dev/ODataV4/$metadata/$metadata metadata URL
To troubleshoot such errors, Fiddler or another debugging proxy can be used to intercept HTTP calls and inspect what gets sent and what is actually returned by the server.
After a while of research and testing, I managed to get this working. Indeed the requested resource (CSDL) could not be requested because of a lack of permissions. But the CSDL can be requested using a HttpClient initially, which takes the supplied credentials into account:
public class Container : DataServiceContext
{
public override ICredentials Credentials { get => base.Credentials; set => throw new NotSupportedException(); }
public bool UseDefaultCredentials { get; }
public DataServiceQuery<Person> People { get; }
private static IEdmModel? ParsedModel;
public Container(Uri serviceRoot, ICredentials? credentials = null, bool useDefaultCredentials = false) : base(serviceRoot)
{
if (serviceRoot is null) throw new ArgumentNullException(nameof(serviceRoot));
base.Credentials = useDefaultCredentials ? credentials ?? CredentialCache.DefaultCredentials : credentials;
UseDefaultCredentials = useDefaultCredentials;
People = base.CreateQuery<Person>(nameof(People));
// It is required to load the service model initially.
Format.LoadServiceModel = () => RequestModel();
Format.UseJson();
}
// This method requets the service model directly from the OData endpoint via HttpClient.
// It also uses the supplied credentials.
private IEdmModel RequestModel()
{
if (ParsedModel is not null) return ParsedModel;
// Create http client (+ handler) populated with credentials.
using HttpClientHandler clientHandler = new()
{
Credentials = UseDefaultCredentials ? CredentialCache.DefaultCredentials : Credentials,
CheckCertificateRevocationList = true
};
using HttpClient httpClient = new(clientHandler)
{
BaseAddress = new UriBuilder()
{
Scheme = BaseUri.Scheme,
Host = BaseUri.Host,
Port = BaseUri.Port
}.Uri
};
// Process the response stream via XmlReader and CsdlReader.
using var responseStream = httpClient.GetStreamAsync(
new Uri(Path.Combine(BaseUri.AbsolutePath, "$metadata"), UriKind.Relative))
.ConfigureAwait(false).GetAwaiter().GetResult();
using var xmlReader = XmlReader.Create(responseStream);
if (!CsdlReader.TryParse(xmlReader, out var model, out var errors))
{
StringBuilder errorMessages = new();
foreach (var error in errors) errorMessages.Append(error.ErrorMessage).Append("; ");
throw new InvalidOperationException(errorMessages.ToString());
}
// Return and cache the parsed model.
return ParsedModel = model;
}
}
By then it can be used the following:
Container context = new Container(new Uri("https://targetservice.dev/ODataV4/"), useDefaultCredentials: true);
UPDATE
This seems to be a bug in the implementation not forwarding credential information to the metadata request. The following stacktrace shows the call of the LoadServiceModelFromNetwork method:
at Microsoft.OData.Edm.Csdl.CsdlReader.Parse(XmlReader reader)
at Microsoft.OData.Client.DataServiceClientFormat.LoadServiceModelFromNetwork()
at Microsoft.OData.Client.DataServiceClientFormat.get_ServiceModel()
at Microsoft.OData.Client.RequestInfo..ctor(DataServiceContext context)
at Microsoft.OData.Client.DataServiceRequest.CreateExecuteResult(Object source, DataServiceContext context, AsyncCallback callback, Object state, String method)
at Microsoft.OData.Client.DataServiceRequest.Execute[TElement](DataServiceContext context, QueryComponents queryComponents)
at Microsoft.OData.Client.DataServiceQuery`1.Execute()
The current implementation within the package is the following:
internal IEdmModel LoadServiceModelFromNetwork()
{
DataServiceClientRequestMessage httpRequest;
BuildingRequestEventArgs requestEventArgs = null;
// test hook for injecting a network request to use instead of the default
if (InjectMetadataHttpNetworkRequest != null)
// ...
else
{
// ...
httpRequest = new HttpClientRequestMessage(args);
}
// ...
Task<IODataResponseMessage> asyncResponse =
Task<IODataResponseMessage>.Factory.FromAsync(httpRequest.BeginGetResponse, httpRequest.EndGetResponse,
httpRequest);
IODataResponseMessage response = asyncResponse.GetAwaiter().GetResult();
// ...
using (StreamReader streamReader = new StreamReader(response.GetStream()))
using (XmlReader xmlReader = XmlReader.Create(streamReader))
{
return CsdlReader.Parse(xmlReader);
}
}
As it turns out the httpRequest variable here is responsible for handling the actual response. The constructor is implemented as follows:
public HttpClientRequestMessage(DataServiceClientRequestMessageArgs args)
: base(args.ActualMethod)
{
_messageStream = new MemoryStream();
_handler = new HttpClientHandler();
_client = new HttpClient(_handler, disposeHandler: true);
_contentHeaderValueCache = new Dictionary<string, string>();
_effectiveHttpMethod = args.Method;
_requestUrl = args.RequestUri;
_requestMessage = new HttpRequestMessage(new HttpMethod(this.ActualMethod), _requestUrl);
// Now set the headers.
foreach (KeyValuePair<string, string> keyValue in args.Headers)
{
this.SetHeader(keyValue.Key, keyValue.Value);
}
}
Neither the credentials nor the boolean UseDefaultCredentials is forwarded to the HttpClientHandler. But the args provide this information.
Also the credentials are not set after construction and the response is not checked for an invalid status code so that it ends up in this strange behavior.

Add WCF custom certificate validation programatically

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?

Authentication and RequireRole with ServiceStack

I'm trying to create a simple web service with ServiceStack. On the server (service) side, I've first created a user "administrator" and assigned it the "Admin" role. I'm using the ServiceStack built-in credentials authentication and I it authenticates nicely. However, any subsequent call to any webservice that is decorated with the [Authenticate] attribute returns a the following error:
No configuration was added for OAuth provider 'basic'
FYI: the database is RavenDb - using ServiceStack.Authentication.RavenDb. I also registered a MemoryCacheClient in the App_Start.AppHost.Configure(). Let me know if you need to see all code in App_Start.AppHost class.
Server Code (excerpt):
namespace MyTest
{
[Route("/hello")]
[Route("/hello/{Name}")]
[Authenticate]
public class Hello
{
public string Name { get; set; }
}
public class HelloResponse
{
public string Result { get; set; }
}
public class HelloService : Service
{
public IUserAuthRepository UserAuthRepo { get; set; }
public object Any(Hello request)
{
return new HelloResponse { Result = "Hello, " + request.Name };
}
}
}
Client side:
var client = new JsonServiceClient("http://127.0.0.1:65385/auth/credentials?format=json");
var auth = new Auth { UserName = "administrator", Password = "12345" };
var authResponse = client.Post(auth);
if (authResponse.ResponseStatus.ErrorCode == null)
{
client = new JsonServiceClient("http://127.0.0.1:65385/hello");
client.UserName = "administrator";
client.Password = "12345";
// When sending the request - it returns "Not Found"
var helloResponse = client.Send(new Hello { Name = "John" });
}
EDIT:
The web.config of my services looks exactly like written in section 2a of the Hello World tutorial
Here's the configuration of my AppHost:
public override void Configure(Funq.Container container)
{
container.Register(new MemoryCacheClient { FlushOnDispose = false });
container.Register(new EmbeddableDocumentStore { DataDirectory = "Data" }.Initialize());
ConfigureAuth(container);
}
private void ConfigureAuth(Funq.Container container)
{
var appSettings = new AppSettings();
Plugins.Add(new AuthFeature(() => new CustomUserSession(),
new IAuthProvider[] {
new CredentialsAuthProvider(appSettings)}));
Plugins.Add(new RegistrationFeature());
container.Register(
new RavenUserAuthRepository(c.Resolve()));
}
Firstly you should look at ServiceStack's AuthTests for some examples that test authentication with ServiceStack's C# Service Clients.
You're passing the wrong urls in your C# ServiceStack client, i.e. you should only pass in the Base URI where ServiceStack is hosted, e.g:
var client = new JsonServiceClient(
"http://127.0.0.1:65385/auth/credentials?format=json");
var client = new JsonServiceClient("http://127.0.0.1:65385/hello");
Should instead only be:
var client = new JsonServiceClient("http://127.0.0.1:65385/");
var client = new JsonServiceClient("http://127.0.0.1:65385/");
You should also never put ?format=json when using a ServiceStack C# ServiceClient like JsonServiceClient, since the format is already implied in the client which ensures JSON is returned by sending the Accept: application/json HTTP Header upon every request.
OK, found out what the problem was: The [Authenticate], [RequiredRole] and [RequiredPermission] attributes work with Basic Authentication (Authenticate works with Digest Authentication as well). So if you want to use any of those attributes, you must make sure that you have added the BasicAuthProvider to the list of IAuthProviders when setting up the AuthFeature as a plugin. So,
Plugins.Add(new AuthFeature(() => new CustomUserSession(),
new IAuthProvider[] {
new CredentialsAuthProvider(appSettings)}));
must be
Plugins.Add(new AuthFeature(() => new AuthUserSession(),
new IAuthProvider[] {
new CredentialsAuthProvider(appSettings),
new BasicAuthProvider()
}));
I still have to send username and password in clear text with every call though ...
Make sure you register an ICacheClient in your ServiceStack application and then you shouldn't have to send the user name and password with each request. Your client side should be able to handle storing a cookie without any effort on your end.

HttpOperationHandlerFactory and InputParameters issue

I've written a custom OperationHandler for my WCF WebAPI project as follows:
public class AuthenticationOperationHandler : HttpOperationHandlerFactory
{
protected override Collection<HttpOperationHandler> OnCreateRequestHandlers(ServiceEndpoint endpoint, HttpOperationDescription operation)
{
var baseHandlers = base.OnCreateRequestHandlers(endpoint, operation);
if (operation.InputParameters.Where(p => p.Name.ToLower() == "username").Any() &&
operation.InputParameters.Where(p => p.Name.ToLower() == "password").Any())
{
baseHandlers.Add(new AuthenticateRequestHandler(string.Format("{0}:{1}", operation.InputParameters.Where(p => p.Name == "username").First ().Name, operation.InputParameters.Where(p => p.Name == "password").First().Name)));
}
else
{
throw new WebFaultException(HttpStatusCode.Forbidden);
}
return baseHandlers;
}
}
As well as this custom RequestHandler which is added to the pipeline:
public class AuthenticateRequestHandler : HttpOperationHandler<HttpRequestMessage, string>
{
public AuthenticateRequestHandler(string outputParameterName)
: base(outputParameterName)
{
}
public override string OnHandle(HttpRequestMessage input)
{
var stringValue = input.Content.ReadAsString();
var username = stringValue.Split(':')[0];
var password = stringValue.Split(':')[1];
var isAuthenticated = ((BocaMembershipProvider)Membership.Provider).ValidateUser(username, password);
if (!isAuthenticated)
{
throw new WebFaultException(HttpStatusCode.Forbidden);
}
return stringValue;
}
}
and this is my API Implementation:
[ServiceContract]
public class CompanyService
{
[WebInvoke(UriTemplate = "", Method = "POST")]
public bool Post(string username, string password)
{
return true;
}
}
My configuration in Global.asax file is
public static void RegisterRoutes(RouteCollection routes)
{
var config = HttpHostConfiguration.Create().SetOperationHandlerFactory(new AuthenticationOperationHandler());
routes.MapServiceRoute<AuthenticationService>("login", config);
routes.MapServiceRoute<CompanyService>("companies", config);
}
When trying to send a POST request to /companies I receive the following error message:
The HttpOperationHandlerFactory is unable to determine the input
parameter that should be associated with the request message content
for service operation 'Post'. If the operation does not expect content
in the request message use the HTTP GET method with the operation.
Otherwise, ensure that one input parameter either has it's
IsContentParameter property set to 'True' or is a type that is
assignable to one of the following: HttpContent, ObjectContent1,
HttpRequestMessage or HttpRequestMessage1.
on this line:
var baseHandlers = base.OnCreateRequestHandlers(endpoint, operation);
Any idea why this happens and how to fix this in order to force user send username/password parameters in each and every request and validate it against the Membership API afterwards?
To answer your question, your UriTemplate property is empty, which is why an exception is thrown. It should be set as follows:
UriTemplate = "&username={username}&password={password}"
There's still a bug in your code because both input parameters receive the same string, namely username=JohnDoe:password=qwerty
To solve your problem, this is a good article on how to implement HTTP basic authentication with WCF Web API.

WCF Data Service authentication

-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)

Categories

Resources