I'm exploring using ServiceStack as an alternative to WCF. One of my requirements is that the server and client must mutually authenticate using certificates. The client is a service so I cannot use any type of authentication that involves user input. Also the client needs to be able to run on Linux using mono so Windows authentication is out.
I've bound my server certificate to the port of my server using netsh.exe, validated the client is getting the server certificate and data is being encrypted using wireshark. However I can't for the life of me figure out how to configure the server to require a client certificate.
Some people suggested using request filters to validate the client certificate, but that seems very inefficient since every request would check the client certificate. Performance is a very high priority. Creating a custom IAuthProvider seems promising, but all the documentation and examples are oriented to authentication types that involve user interaction at some point and not certificates.
https://github.com/ServiceStack/ServiceStack/wiki/Authentication-and-authorization
Is it possible to use certificates to mutually authenticate the client and server with a self-hosted ServiceStack service?
Here is my test service for reference.
public class Host : AppHostHttpListenerBase
{
public Host()
: base("Self-hosted thing", typeof(PutValueService).Assembly)
{
//TODO - add custom IAuthProvider to validate the client certificate?
this.RequestFilters.Add(ValidateRequest);
//add protobuf plugin
//https://github.com/ServiceStack/ServiceStack/wiki/Protobuf-format
Plugins.Add(new ProtoBufFormat());
//register protobuf
base.ContentTypeFilters.Register(ContentType.ProtoBuf,
(reqCtx, res, stream) => ProtoBuf.Serializer.NonGeneric.Serialize(stream, res),
ProtoBuf.Serializer.NonGeneric.Deserialize);
}
public override void Configure(Funq.Container container)
{}
void ValidateRequest(IHttpRequest request, IHttpResponse response, object dto)
{
//TODO - get client certificate?
}
}
[DataContract]
[Route("/putvalue", "POST")]
//dto
public class PutValueMessage : IReturnVoid
{
[DataMember(Order=1)]
public string StreamID { get; set; }
[DataMember(Order=2)]
public byte[] Data { get; set; }
}
//service
public class PutValueService : Service
{
public void Any(PutValueMessage request)
{
//Comment out for performance testing
Console.WriteLine(DateTime.Now);
Console.WriteLine(request.StreamID);
Console.WriteLine(Encoding.UTF8.GetString(request.Data));
}
}
Just like in server certificate authentication, client certificate authentication makes use of digital signatures. For a client certificate to pass a server's validation process, the digital signature found on it should have been signed by a CA recognized by the server. Otherwise, the validation would fail.
Some people suggested using request filters to validate the client certificate, but that seems very inefficient since every request would check the client certificate. Performance is a very high priority.
REST is stateless so if you are not willing to check the client certificate on each request you would need to provide an alternative authentication token to show a valid identity has already been provided.
So you can avoid checking the certificate on subsequent requests, if after authenticating the client certificate, you provide the client with a session Id cookie that can verified instead.
However I can't for the life of me figure out how to configure the server to require a client certificate.
The client certificate is only available on the original http request object which means you have to cast the request object to access this value. The code below is given for casting the request to a ListenerRequest
which is used by the self hosting application.
A request filter will check:
First for a valid session cookie, which if valid will allow the request without further processing, so does not require to verify the client certificate on subsequent requests.
If no valid session is found, the filter will attempt to check the request for a client certificate. If it exists try to match it based on some criteria, and upon acceptance, create a session for the client, and return a cookie.
If the client certificate was not matched throw an authorisation exception.
GlobalRequestFilters.Add((req, res, requestDto) => {
// Check for the session cookie
const string cookieName = "auth";
var sessionCookie = req.GetCookieValue(cookieName);
if(sessionCookie != null)
{
// Try authenticate using the session cookie
var cache = req.GetCacheClient();
var session = cache.Get<MySession>(sessionCookie);
if(session != null && session.Expires > DateTime.Now)
{
// Session is valid permit the request
return;
}
}
// Fallback to checking the client certificate
var originalRequest = req.OriginalRequest as ListenerRequest;
if(originalRequest != null)
{
// Get the certificate from the request
var certificate = originalRequest.HttpRequest.GetClientCertificate();
/*
* Check the certificate is valid
* (Replace with your own checks here)
* You can do this by checking a database of known certificate serial numbers or the public key etc.
*
* If you need database access you can resolve it from the container
* var db = HostContext.TryResolve<IDbConnection>();
*/
bool isValid = certificate != null && certificate.SerialNumber == "XXXXXXXXXXXXXXXX";
// Handle valid certificates
if(isValid)
{
// Create a session for the user
var sessionId = SessionExtensions.CreateRandomBase64Id();
var expiration = DateTime.Now.AddHours(1);
var session = new MySession {
Id = sessionId,
Name = certificate.SubjectName,
ClientCertificateSerialNumber = certificate.SerialNumber,
Expires = expiration
};
// Add the session to the cache
var cache = req.GetCacheClient();
cache.Add<MySession>(sessionId, session);
// Set the session cookie
res.SetCookie(cookieName, sessionId, expiration);
// Permit the request
return;
}
}
// No valid session cookie or client certificate
throw new HttpError(System.Net.HttpStatusCode.Unauthorized, "401", "A valid client certificate or session is required");
});
This used a custom session class called MySession
, which you can replace with your own session object as required.
public class MySession
{
public string Id { get; set; }
public DateTime Expires { get; set; }
public string Name { get; set; }
public string ClientCertificateSerialNumber { get; set; }
}
The client needs to set it's client certificate to send with the request.
var client = new JsonServiceClient("https://servername:port/");
client.RequestFilter += (httpReq) => {
var certificate = ... // Load the client certificate
httpReq.ClientCertificates.Add( certificate );
};
Once you have made the first request with the server your client will receive a session Id cookie, and you can optionally remove the client certificate from being sent, until the session becomes invalid.
I hope that helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With