I have a simple personal expense tracking app that for the last few months I’d been working on and off to migrate away from ASP.NET Core MVC towards more modern client apps whilst reusing the backend code and logic. I can’t just shut off the MVC app whilst I take my time to build its replacement, because its still being used. But if I can build an HTTP layer on top of the domain and start exposing the endpoints there, then I can build out the client apps side-by-side, test them, tune them etc before eventually turning off the lights on the MVC app.
So over the last few months that’s what I have managed to do: built a backend RESTful-ish API that can power clients in a standard way, built a Single Page App (SPA) for in-browser usage and built a mobile app using Android Xamarin Forms for on-the-go tracking of expenses (this had been a plan of mine for a long time which has finally come to fruition, it just kept getting put off for one reason or another so I am really happy about this. I might write a separate post for this later).
The evolution for what is essentially a very simple app, over the years has looked like this:
I started with a noddy monolithic VB6 WinForms app running on my machine backed by an Access database over 15 years ago and ended up with C#/.NET Core, full CI/CD, cloud hosted (Azure App Service), fairly neatly layered, replete with HTTP API and almost “plug-n-play” client apps with room for more. The journey has been fun, seeing a simple system evolve so much over such a long time! 😀
In this 3-part blog post I will share some of my experiences of this migration journey.
Essentially the new super-sophisticated solution architecture, was going to be:
Porting the API Controllers
The first step was to copy over all the existing MVC controllers over to the new Web API project and get rid of the view and ASP.NET session specific stuff. Server side in-memory session state makes it hard to reason about code and in worst case could hinder API’s scalability. Given that I intended to have the clients maintain their own sessions, server side session state is pointless. I didn’t want to put this state in a distributed store like Redis or something to keep costs low.
The port was fairly straightforward since almost all the controllers are thin controllers i.e. they delegate the bulk of the work to the use case/application services layer which is already covered by tests. I blogged about re-factoring the application using some of the principles of DDD, a couple of years ago. Most of the constructs that made this porting exercise fairly trivial were established back then – bounded context, aggregates etc. This is really the essence of clean architecture, it makes future changes (that are inevitable yet not always foreseeable) lot easier by reducing their inherent risk.
Securing the API
Now that I have a RESTful-ish API that will be accessed over the public internet, I need to secure it to some extent. I am not a security expert but there are couple of standard approaches that can be used to secure REST APIs:
- API Keys: a server defined API key can be shared with the client that it can include in an HTTP header that can be validated server side using a middleware and request be rejected if the key is invalid or if the header is not present and allowed to go through, if the key is valid.
The problem with this approach is that the client side application will be sending the API key in plain-text which kinda defeats the purpose. You could encrypt the key on client side and decrypt on the server, but it still doesn’t help as someone could still hijack it and replay it against the API. As long as the encrypted string is valid, the server would happily allow the request through. API keys might be fine for service to service authentication but not so much for client apps which run on user devices and rely on client side storage.
- JWT Auth Tokens: This approach uses cryptographically signed JSON Web Tokens that can be issued as a result of a successful authentication. The token can contain information like user claims or any other non-sensitive information. Once this token is handed back to the client post-login, the client then needs to include this token in every subsequent request in the HTTP Authorization header which the server will validate.
The JWT standard allows you to define a token expiry timestamp that any compliant JWT validator can verify and reject the token if the timestamp has expired. This can be used to reduce the attack surface of the token.
JWTs can also be used with ASP.NET cookies by packaging the token inside a cookie and then encrypting the whole thing. But since I am not using cookies, I can use the pure JWT approach. I have written about JWTs in the past.
JWTs are signed using a symmetric secret key that is and should be, only known to the server, this will help reduce the attack surface further because even if an attacker was to get hold of the token, they cannot reverse engineer it to get to the signing key. Token replay attacks are still possible, at least for as long as the token is valid which is where techniques like auto-refreshing using refresh token exchange can be useful. For this app I opted to keep it simple for now and instead chose limited validity JWTs. This blog series is not meant to be a guidance on API security best practices, for that, check this, this and this…and more.
In part 3 I will talk more about how authentication is wired up on the client side!
Building an API Client Package
Now that I have my API ready, I have two options to consume it from the client apps.
- Use the
HttpClientdirectly in the client apps to make calls to backend API and do this for all the C# based client apps that I might end up writing. (if I ever want to switch to JS/TS apps, I might expose a JS client as well) Or
- Create a API client package that’s re-usable across C# client apps thus reducing the cognitive overhead there. I know Refit kinda operates in this area and you can create strongly typed clients almost declaratively but I fail to see the appeal of depending on a third party package for something this simple and custom.
So I opted to go the C# API client package way for the following reasons:
- Cross cutting concerns like exception handling, transient fault handling/resilience (retries, timeouts etc), automatic authentication header inclusion etc can all be encapsulated in the client, freeing up the consumer app from all these concerns.
- Construction of the client instance can also be exposed in a minimal way without requiring a whole lot of ceremony or requiring the consumer app to know exactly how the client needs to be constructed.
- Makes testing for consumers easier by exposing a public interface that can be created test doubles for easily. An extension here could be to provide a fake client for testing purposes, although for now I chose not to do it to keep it simple since I will be writing the consumer apps and I can run the API locally and just target that. Fake clients (and fake service) might make sense for third party consumers of which I have precisely zero.
One of the key design decisions I made was to let the consumer provide a token persistence mechanism of their choice (as dictated by the interface that I exposed from the client shown below), that way the API client package can put token in it during the auth flow and get token out of it during data requests and stamp it on the outgoing requests. In the very first version I’d used a private static variable to hold this token but that made testing a lot more hassle due to implicit dependency on the static. By default it will just use an in-memory store that’s backed by a thread safe
|/// A durable and secure place to store authentication token into and get out of|
|public interface IAuthTokenRepository|
|/// Get the currently active auth token. Implementations MUST decide on the appropriate key|
|/// for the persistance record. The interface assumes that only one token will be returned|
|/// from the store.|
|/// <returns><see cref="AuthToken"/></returns>|
|/// Store the freshly generated auth token. Implementations MUST decide on the appropriate key|
|/// for the persistance record.|
|/// <param name="token">The <see cref="AuthToken"/> to store</param>|
|/// <returns><see cref="Task"/></returns>|
|Task Save(AuthToken token);|
At least this way the client apps can provide the storage they think is the best without being tied to hard coded in memory version.
Testability concerns of the API client package revolved around:
- Does the client throw an exception when the authentication fails?
- Does the client do appropriate retries in case of HTTP failures?
- Does the client timeout for requests that take too long?
- Does the client appropriately map the API requests/responses to strongly typed DTOs? For e.g. are two
intfields swapped around?
I have written about some of the heuristics and styles I use (personally and in my team at work), that I employed whilst writing tests for this client as well.
Now that the backend is ready and a API client to go with it, in part 2 I’ll tackle Blazor!