Web application security is always an important part of how you design and implement a solution. For many years, the standard method for securing a C# ASP .NET MVC web application was to use session for storing the user object, in combination with traditional .NET forms-based authentication cookies. This has worked well for many years, and still does. However, more recently, single page web applications have gained significant traction. Since these types of web applications run almost entirely in the client web browser, they often call a REST Web API or .NET MVC controller, serving as a web service API, in order to get and send data to display in the views. In this scenario, a large portion of the business logic has moved from the server-side to the client-side. Still, the API calls to access data need to be secured.
In this tutorial, we’ll walk through how to create a simple, but effective token-based authentication framework to secure a .NET REST API. The framework is based upon a detailed post, using the hash-based message authentication code (HMAC).
What’s so bad about .NET session anyway? Session allows you to store objects on the server, in memory, or persisted on a database. Aside from the potential clutter it can cause if mismanaged, it has a more recent shortcoming, largely involving the cloud.
Companies are migrating to the cloud en masse. Along with this migration, certain features used by traditional C# ASP .NET MVC web applications are becoming more difficult to migrate. One of these is session-backed storage.
Cloud servers, by nature, are often temporary instances. They can be transparently brought up and taken down, sometimes without notice. When a server instance is removed or re-created, it can lose persisted data, which includes session storage - both in memory and on a database. While there are workarounds for migrating session storage to cloud compatibility, many companies are choosing to re-architect their web applications to a REST design, providing web service API calls to the user interface. Along with this change, they’re choosing to move away from session, and instead use token-based authentication.
Token-based authentication involves providing a token or key in the url or HTTP request header, which contains all necessary information to validate a user’s request. Some examples of information included in the token are username, timestamp, ip address, and any other information pertinent towards checking if a request should be honored.
In this tutorial, we’ll focus on a simple implementation of token-based authentication. Let’s see how it works.
First, we’ll need to decide exactly what we want in our token. We’ll use the following schema:
A token is generated by hashing a message with a key, using HMAC SHA256.
We include the client IP address and user-agent string as part of the message in order to bind a token to a specific client. This helps prevent an attacker from replaying a token request, as both pieces of data are verified at the server and must match the requesting client information. If the client web browser making the API request is different than the one that generated the token, then the token will fail to authenticate.
The resulting token is concatenated with username:timeStamp and the final result is base64-encoded. This final concatenation is necessary so that we are able to validate the timestamp, as well as check the user’s credentials on the server.
There are several vectors of attack that need to be considered. The first, is a simple replay attack or man-in-the-middle attack. This involves an attacker capturing a token API request and replaying the same exact request again. We can prevent this type of attack by validating client-specific data as part of the token (IP address and user-agent string). In addition, adding a token expiration date helps to limit the duration that such an attack is viable. You could further prevent this type of attack by keeping a server log (MemoryCache, etc) of recently used tokens and invalidate them once used. Depending on how short the token expiration time is (5-10 minutes), invalidation may not be necessary.
Of course, SSL is another way to help protect tokens. SSL encrypts url query string parameters and post data. Since the token is included as a parameter within the url, SSL can be an effective tool to boost token authentication security.
In the above code for GenerateToken(), notice that we provide a username, password, IP address, user-agent, and timestamp (in ticks). Using this information, we can generate a time-sensitive token that is bound to a specific IP address and web browser. It’s also important to note that we utilize a hashed password, rather than the actual user password.
Notice that the final token actually has two parts. There is a hashed portion, which includes sensitive details. There is also a public portion (base64-encoded) that includes necessary public data for the server to validate the token. When validating, we’ll extract the username and timestamp and lookup the remaining fields in order to authenticate.
Every API call will contain a token as part of the url. To validate a token we can follow a series of steps. First, we base64-decode the string. This provides us with the token, along with the username and timestamp. We can then validate the timestamp to ensure it has not yet expired. If the timestamp is valid, we’ll next lookup the user in our database to obtain their hashed password or other unique identifying key. With this information, we can now compute a token and verify that it matches the one passed to our API. We do this by hashing the username:ip:userAgent:timeStamp with the key hashed password:salt. We can then compare the computed token and ensure that it matches the provided one.
The method for checking a valid token is included below.
To validate a token, we must be able to compute one ourselves and compare it to the one supplied by the client. Our GenerateToken() method requires 5 parameters: username, password, ip, userAgent, and ticks. So, where do we get this information?
First, note that the IsValidToken() method receives a token, IP address, and user-agent string. All three fields are passed to us by the .NET MVC controller. The token is passed as part of the url, while the IP and user-agent are determined from the HTTP request object. This ensures that we are attempting to compute a token based upon the actual client making the API call. This effectively gives us two of the required parameters for computing a token.
Since the provided token, itself, consists of two pieces (a hashed part and a public username:timestamp part), we can easily extract two more parameters. This gives us the username and ticks.
We now only need one more parameter: the user’s hashed password (or other unique identifier). We can obtain this on the server by looking up the user’s account from the database. It’s important to note that the user’s password is never transferred over the wire. It is looked up on the server or entered by the user in the client web browser, but never transferred between.
With all 5 parameters obtained, we can now call GenerateToken() and compare the result.
We now have methods for generating and validating tokens. However, how does the token get passed to these methods? We know that our client will be calling REST web service API methods. Since our REST web service is an MVC .NET controller, we can check the token parameter in each controller method.
An example C# MVC .NET web service API controller method is included below:
The above code shows an example of a simple MVC controller method. The method takes a string as a parameter and returns a JSON response. We can assume the REST url for calling this method would be, as follows:
The controller method is only concerned with receiving a parameter, “query”. We can still pass the token as a url parameter and have it validated as well. We do this with the RESTAuthorize attribute. It’s added at the top of the MVC controller class and will be effective on all controller methods within the class.
The RESTAuthorizeAttribute specifically looks for the token url parameter and calls the IsTokenValid() method to authorize it. It handles sending the appropriate data from the client request, including the IP address, user-agent, and any other data pieces that need to be validated.
The attribute is defined as follows:
Notice, in the above code we call IsTokenValid() from within the Authorize() method. This allows us to protect our C# ASP .NET MVC controller, while keeping the token-based authentication parameter separate from the actual controller methods.
We now have a fully functioning token-based authentication framework. The only part missing is to allow a token to be generated by the client.
Also include the crypto-js library in your main page:
When the user initially logs into the web application, they will provide their username and password for authentication. Since we’re not validating with traditional session ASP .NET forms-based authentication, we will authenticate via a token instead.
The above method allows us to determine the web browser client IP address. Note, we include a check on the HTTP header value “X-Forwarded-For”, in case the web application is behind an AWS cloud instance or load balancer. If not, we default to using the standard Request.UserHostAddress value.
The following example shows how to use the code to generate a token. The initial call from the login page generates the key:
Of course, to login, an API call isn’t even needed. Simply call the generate(username, password) method to have a key created on the client, usable by subsequent API calls:
Subsequent calls to the web service simply omit the username and password, as upon leaving the login page, the password is no longer in memory.
Notice that each call to SecurityManager.generate() creates a unique token, each with its own timestamp. Each token is only valid for a short duration of time (ie., 5-10 minutes) before it is expired. If the client provides a different timestamp as part of the public portion of the token, or if the client provides a different IP address or user-agent than the one contained within the token body, it will fail to match the hashed message portion of the token, and thus, access will be denied.
Developers have a variety of options for securing web applications. Two popular options include session-backed forms authentication with cookies and token-based authentication via the url. While both options offer a secure solution for a C# ASP .NET MVC web application, token-based authentication excels, in particular, with cloud-compatibility. Token-based frameworks also offer an advantage in striving for a stateless REST web service, compared with utilizing session for maintaining application/user state.
Download the project source code on GitHub by visiting the project home page.
This article was written by Kory Becker, software developer and architect, skilled in a range of technologies, including web application development, machine learning, artificial intelligence, and data science.