How to authenticate a client using a certificate in ServiceStack?
Asked Answered
C

1

9

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));
    }
}
Chelyabinsk answered 15/7, 2014 at 1:58 Comment(0)
A
11

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.

Server Process:

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; }
}

Client Process:

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.

Abohm answered 15/7, 2014 at 12:41 Comment(1)
That helps a ton! Thanks for the very thorough response!Chelyabinsk

© 2022 - 2024 — McMap. All rights reserved.