.Net, Azure and occasionally gamedev

SignalR on .Net Core

2018/05/31

I have been slowly working on the new homeapp infrastructure implemented in .Net Core where signalR will play an integral role of relaying commands.

SignalR allows easy two-way communication between server and clients. However it wasn't available for .Net Core up until yesterday when .Net Core 2.1 was officially released.

Unfortunately SignalR 1.0 for .Net Core is not compatible with the old version of SignalR (targeting .Net 4.5) but since all my clients are built on .Net Standard 2.0 this wasn't a problem.

SignalR security

The focus of this of this post will be on securing the connections.

Writting a sample chat app (like all the demos do) is quite simple and even realtime multiplayer games can be implemented with very little code. However most demos never show the security aspects.

Requirements

For the new homeapp implementation I intend to use signalR for communication between all parties:

Obviously security is a primary concern with this model as I need to protect not only against possible outside attacks but also need to ensure that the channels are properly secured from each other. With poor security an attacker could gain access to someone elses signalR channel and transmit a command to open their front door!

SignalR on .Net Core has simplified its internal structure and uses https by default and each connection receives a randomised connectionId which makes it hard to guess the connection id.

This helps against obvious spoofing attacks but there are quite a few more attack vectors.

Authentication and Authorization

SignalR seamlessly integrates with whichever authentication model you use in Asp.Net Core. As such you can use the Authorize attribute on your hub with Roles and signalR will respect them.

For user interactions I have already secured the apis with OAuth 2 providers so users can login with their existing accounts (Microsoft, Google).

Since I also require the raspberry pi hubs to connect via signalR I needed another mechanism as these hubs will run without a user context.

I decided to unify the signalR authentication and protect it with Json Web Tokens (Jwt).

These tokens can be generated based off the OAuth providers for (app) users or generated based of private access tokens that the user has to enter one time when setting up their hub.

Jwt

Json Web Tokens have become an industry standard for stateless authentication between client and server. I won't describe them in too much detail here but basically each token is "self contained". It contains both a set of claims as well as a signature proving its validity. This allows the server to trust them without having to check the database on each request.

Setting them up for authentication in .Net Core is straightforward.

I added the authentication code in startup:

app.UseAuthentication();

and

services.AddAuthentication()
        .AddJwtBearer(options =>
        {
            options.RequireHttpsMetadata = true;
            options.SaveToken = true;
            options.TokenValidationParameters = new TokenValidationParameters
            {
                RequireExpirationTime = true,
                RequireSignedTokens = true,
                ValidateIssuerSigningKey = true,
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Authentication:Jwt:SigningKey"])),
                ValidIssuer = Configuration["Authentication:Jwt:Issuer"],
                ValidAudience = Configuration["Authentication:Jwt:Audience"]
            };
            options.Events = new SignalrJwtBearerEvents();
        });

Where SignalrJwtBearerEvents is implemented like this:

public class SignalrJwtBearerEvents : JwtBearerEvents
{
    public override Task MessageReceived(MessageReceivedContext context)
    {
        if (context.Request.Path.Value.StartsWith("/signalr") &&
            context.Token == null &&
            (context.Request.Headers.TryGetValue("Authorization", out StringValues token) ||
            context.Request.Query.TryGetValue("access_token", out StringValues token2)))
        {
            // pull token from header or querystring; websockets don't support headers so fallback to query is required
            var tokenValue = token.FirstOrDefault() ?? token2.FirstOrDefault();
            const string prefix = "Bearer ";
            // remove prefix of header value
            if (tokenValue?.StartsWith(prefix) ?? false)
            {
                context.Token = tokenValue.Substring(prefix.Length);
            }
            else
            {
                context.Token = tokenValue;
            }
        }

        return Task.CompletedTask;
    }
}

This implementation will use the usual Authentication header where possible. The fallback to the querystring value is necessary for javascript clients using websockets from inside a browser since they don't support headers there. I have not implemented such a client yet (the C# Websocket client supports headers just fine) but I decided to leave in the fallback as a good measure.

Hub setup

[Authorize(AuthenticationSchemes = "Bearer")]
public class CommandHub : Hub
{
    public async Task ReceiveCommandAsync(string command)
    {
        // process
    }
    
    [Authorize(Role = "Admin")]
    public async Task SendCommandAsync(string user, string command)
    {
        var connectionId = GetFromUser(user);
        await Clients.Client(connectionId).SendCommandAsync(command);
    }
}

The hub above is just a simplified version.

The SendCommandAsync method should obviously only be called from authorized clients (e.g. I shouldn't be able to send an "OPEN_DOOR" command to your house) but the code above shows that the hub is protected by bearer tokens that are validated and each method can then also have additional role definitions.

On the client side SignalR has the AccessTokenProvider factory method which allows token generation for each connection:

var builder = new HubConnectionBuilder()
    .WithUrl(url, options =>
    {
        options.AccessTokenProvider = () => _tokenHelper.GenerateTokenAsync();
    });
_hubConnection = builder.Build();

I assumed that a new token would be issued and sent with each request. That way I could check the claims and identify the caller.

However it didn't work that way and it took me a while to figure out why.

During debugging I found that the same token seems to be reused for all messages sent over the channel (AccessTokenProvider factory was only called once).

When calling both hub methods from a client that was not an admin only the "ReceiveCommandAsync" method was invoked whereas the other method return 401, so authorization from the token seemed to work.

To further verify I created a token that expired after one minute and kept calling "ReceiveCommandAsync" every second from the client in a loop.

The commands where received on the server every second even well beyond the jwt expiry time. This happened because only one token was generated by the client AccessTokenProvider factory and said token was only verified once at the start of the connection.

It took me a while but eventually I figured out the reason: While I was testing locally the connection negotiated between my client and server (on the same pc) was always websockets.

Once I temporarily disabled websockets as a transport protocol the connection switched to long polling and with each call, a new token was issued by the AccessTokenProvider factory.

This in turn however means that I can't rely on json web tokens to secure the apis as the expiry time is ignored. This is also discussed in an open issue on their github.

As per the suggestion in the comments I implemented my own expiry check inside the called method:

public async Task ReceiveCommandAsync(string command)
{
    if (!long.TryParse(Context.User.Claims.FirstOrDefault(c => c.Type == "exp")?.Value, out long expiry))
        return;

    var dt = DateTimeOffset.FromUnixTimeSeconds(expiry);
    if (DateTimeOffset.UtcNow > dt)
    {
        Context.Abort();
        return;
    }
}

After testing again with the same shortlived token (1 minute) the connection was aborted.

While it worked it means that after each expiry a new websocket connection must be established. On top of that it makes the jti value useless.

jti (json token id) is a claim intended as a unique value sent with each token to prevent replay attacks: All tokens have both an expiry and a unique id. The server can thus ensure that no command is sent twice within the token expiration timespan by keeping a server side cache of recently used "jit" values. Any commands with duplicate jit values are simply rejected.

However with websockets this wouldn't work because all messages from a websocket connection would keep sending the same initial token and thus the same jti.

To make it work with websockets I would have to either abort the connection after each websocket request (which would make websockets protocol useless), or to send the jwt as part of the message (again only for websockets, as I would otherwise send the token twice: once in the header and once in the body, bloating the size of non-websocket requests).

Clearly this sort of security level was not considered with websockets as all recommendations I find are along the lines of "just send a new jwt over the websocket channel once the old one expires" implying to me that the authors intend to use long lived tokens (e.g. 60 minute sessions) and not unique tokens per request.

Workaround for websockets or custom authentication?

While I could build a workaround solely for websockets where I send the jwt inside each request I decided against it. For me the entire purpose of signalR is to have the transport protocols abstracted away. I want one implementation that "just works" no matter what protocol is used internally.

I decided to do the later. As such I will use a json web token to establish the initial connection as this works for all protocols.

Since I don't plan on adding dynamic roles just yet, an initial token with the access roles will do just fine as far as access protection is concerned.

For the messages I will then implement my own lightweight token system.

Since the initial token already determined who is calling and what rights they have, the messages themself only need expiration + replay protection.

In its simplest form this can be implemented by a wrapper class:

public class SignalRMessage
{
    public DateTimeOffset IssuedAt { get; set; }
    
    public DateTimeOffset Expiry { get; set; }
    
    public string Nonce { get; set; }

    public string Signature { get; set }
    
    public object Message { get; set; }
}

This stripped down implementation is similar to json web tokens and uses the same signature verification method to ensure the message is not tampered with. By skipping the claim fields (as they are already verified when the connection is setup) and the token header (all clients will use Sha256 for signature verification) the message has far less overhead than a json web token.

Without the password generating the signature an attacker won't be able to sign his own message and a message that was alreay sent will be protected against replay attacks by short expiration times and the unique nonce.

Only once the verification was successful will the actual message be queued for processing on the server.

Recap

Overall working with SignalR for .Net Core was as easy to get started as SignalR itself. The authentication issues I had with websockets are annoying but ultimately a problem with the websocket protocol and I'm sure they'll be sorted out within the next few updates of SignalR.

tagged as .Net Core and SignalR