Token-Based Authentication for Web Service APIs in C# MVC .NET

Introduction

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).

Our API will be created from a simple MVC controller. Each call on the API will be protected by token-based authentication, which requires the client to provide a token key in the URL for each request. Each token will be unique and have a limited duration of time that it is valid. We’ll complete the authentication framework by including client-side Javascript for generating the token and making calls to the .NET API service.

Migrating Away From Session

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.

Enter the Cloud

Token-Based Authentication for Web Service APIs

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.

A Brief Overview of 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.

Generating a Token

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.

1
2
The message will consist of: username:ip:userAgent:timeStamp
The key will consist of a hashed combination of: password:salt

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.

A Note on Token Strength .. and Weakness

It’s important to keep in mind the strength and weakness of a token-based system. The most important rule is - never transmit the token key across the wire. That is, you select a key that will be used to generate a token for each request. This key must never be sent to the server and must always remain on the client (ie., held in javascript memory). With that in mind, here are some descriptions of potential weaknesses for token-based authentication.

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.

A second type of attack is physically obtaining the token key. Since the javascript that generates a token is publicly available, an attacker could attempt to generate his own token, impersonating another user. To do this, he would need to provide another user’s username as part of the token body. However, this requires using the user’s key to hash the token message. To generate the user’s key, an attacker would need either the user’s password or their actual key. We’ll have to assume that an attacker would not have access to the user’s password (otherwise, the user could change his password to invalidate the token key). Since we never transmit the token key to the server (the token key never goes over the wire), it’s possible instead that the user’s PC could be physically compromised or stolen. An attacker could access the localStorage, obtain the user’s key, and generate his own token on behalf of the user. Really, this is no different than auto-sign-in, which many web applications already do for the physical computer. We have to consider this case a more remote possibility and leave it to other security mechanisms to protect the physical PC.

Code for Generating a Token

The method for generating a token is included below. Keep in mind, this code will need to be mirrored on the client (such as with javascript) in order to make API calls.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private const string _alg = "HmacSHA256";
private const string _salt = "rz8LuOtFBXphj9WQfvFh"; // Generated at https://www.random.org/strings
public static string GenerateToken(string username, string password, string ip, string userAgent, long ticks)
{
string hash = string.Join(":", new string[] { username, ip, userAgent, ticks.ToString() });
string hashLeft = "";
string hashRight = "";
using (HMAC hmac = HMACSHA256.Create(_alg))
{
hmac.Key = Encoding.UTF8.GetBytes(GetHashedPassword(password));
hmac.ComputeHash(Encoding.UTF8.GetBytes(hash));
hashLeft = Convert.ToBase64String(hmac.Hash);
hashRight = string.Join(":", new string[] { username, ticks.ToString() });
}
return Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Join(":", hashLeft, hashRight)));
}
public static string GetHashedPassword(string password)
{
string key = string.Join(":", new string[] { password, _salt });
using (HMAC hmac = HMACSHA256.Create(_alg))
{
// Hash the key.
hmac.Key = Encoding.UTF8.GetBytes(_salt);
hmac.ComputeHash(Encoding.UTF8.GetBytes(key));
return Convert.ToBase64String(hmac.Hash);
}
}

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.

Validating a Token

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
private const int _expirationMinutes = 10;
public static bool IsTokenValid(string token, string ip, string userAgent)
{
bool result = false;
try
{
// Base64 decode the string, obtaining the token:username:timeStamp.
string key = Encoding.UTF8.GetString(Convert.FromBase64String(token));
// Split the parts.
string[] parts = key.Split(new char[] { ':' });
if (parts.Length == 3)
{
// Get the hash message, username, and timestamp.
string hash = parts[0];
string username = parts[1];
long ticks = long.Parse(parts[2]);
DateTime timeStamp = new DateTime(ticks);
// Ensure the timestamp is valid.
bool expired = Math.Abs((DateTime.UtcNow - timeStamp).TotalMinutes) > _expirationMinutes;
if (!expired)
{
//
// Lookup the user's account from the db.
//
if (username == "john")
{
string password = "password";
// Hash the message with the key to generate a token.
string computedToken = GenerateToken(username, password, ip, userAgent, ticks);
// Compare the computed token with the one supplied and ensure they match.
result = (token == computedToken);
}
}
}
}
catch
{
}
return result;
}

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.

Securing the Web Service Controller

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:

1
2
3
4
5
6
7
8
9
10
11
[RESTAuthorize]
public class ApiController : Controller
{
[HttpGet]
public JsonResult Person(string query)
{
var data = PersonRepository.Find(query);
return Json(data, JsonRequestBehavior.AllowGet);
}
}

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:

http://localhost/api/person?query=Luke?token=R2D2Sqr7c3V0R2dTMxMDAzMTA3MDAwMAC3P0==

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

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class RESTAuthorizeAttribute : AuthorizeAttribute
{
private const string _securityToken = "token"; // Name of the url parameter.
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (Authorize(filterContext))
{
return;
}
HandleUnauthorizedRequest(filterContext);
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
base.HandleUnauthorizedRequest(filterContext);
}
private bool Authorize(AuthorizationContext actionContext)
{
try
{
HttpRequestBase request = actionContext.RequestContext.HttpContext.Request;
string token = request.Params[_securityToken];
return SecurityManager.IsTokenValid(token, CommonManager.GetIP(request), request.UserAgent);
}
catch (Exception)
{
return false;
}
}
}

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.

Generating a Token on the Client

The C# .NET method for generating a token is only used by the IsValidToken() method, as part of computing a token to authenticate. However, the client still needs to generate his own token in order to call the API methods. We can do this in a variety of programming languages, but for this tutorial, we’ll focus on Javascript. This is especially handy for single page applications created with AngularJs or other MVC client frameworks.

We can create a javascript file, which can be included in the main page layout or parent view. The contents of this file will allow generation of a token, as shown below.

security.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
var SecurityManager = {
salt: 'rz8LuOtFBXphj9WQfvFh', // Generated at https://www.random.org/strings
username: localStorage['SecurityManager.username'],
key: localStorage['SecurityManager.key'],
ip: null,
generate: function (username, password) {
if (username && password) {
// If the user is providing credentials, then create a new key.
SecurityManager.logout();
}
// Set the username.
SecurityManager.username = SecurityManager.username || username;
// Set the key to a hash of the user's password + salt.
SecurityManager.key = SecurityManager.key || CryptoJS.enc.Base64.stringify(CryptoJS.HmacSHA256([password, SecurityManager.salt].join(':'), SecurityManager.salt));
// Set the client IP address.
SecurityManager.ip = SecurityManager.ip || SecurityManager.getIp();
// Persist key pieces.
if (SecurityManager.username) {
localStorage['SecurityManager.username'] = SecurityManager.username;
localStorage['SecurityManager.key'] = SecurityManager.key;
}
// Get the (C# compatible) ticks to use as a timestamp. http://stackoverflow.com/a/7968483/2596404
var ticks = ((new Date().getTime() * 10000) + 621355968000000000);
// Construct the hash body by concatenating the username, ip, and userAgent.
var message = [SecurityManager.username, SecurityManager.ip, navigator.userAgent.replace(/ \.NET.+;/, ''), ticks].join(':');
// Hash the body, using the key.
var hash = CryptoJS.HmacSHA256(message, SecurityManager.key);
// Base64-encode the hash to get the resulting token.
var token = CryptoJS.enc.Base64.stringify(hash);
// Include the username and timestamp on the end of the token, so the server can validate.
var tokenId = [SecurityManager.username, ticks].join(':');
// Base64-encode the final resulting token.
var tokenStr = CryptoJS.enc.Utf8.parse([token, tokenId].join(':'));
return CryptoJS.enc.Base64.stringify(tokenStr);
},
logout: function () {
SecurityManager.ip = null;
localStorage.removeItem('SecurityManager.username');
SecurityManager.username = null;
localStorage.removeItem('SecurityManager.key');
SecurityManager.key = null;
},
getIp: function () {
var result = '';
$.ajax({
url: '/ip',
method: 'GET',
async: false,
success: function (ip) {
result = ip;
}
});
return result;
}
};

Also include the crypto-js library in your main page:

1
2
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/rollups/hmac-sha256.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.2/components/enc-base64.js"></script>

The above javascript code allows the client to generate a token for each web service API call that is made. While we’re using javascript, you can generate a token with HMAC in a variety of programming languages. Here’s how it’s used.

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 javascript method generate() may be called, by passing in the username and password, provided by the client. This information is not transmitted to the server and is held completely in the web browser client. The method then computes a hashed token key (bound to an IP address), and uses this to generate a token. For single-page applications (AngularJs, etc) the key can be stored in memory via javascript. For multi-page applications that require a page reload or for further persistence, the key can be stored in localStorage. In either case, the key never leaves the client.

Getting the Client IP Address

The javascript code for generating a token makes a call to the web application to obtain the client IP address. We can use a simple C# MVC .NET helper method and controller with the following code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static class CommonManager
{
public static string GetIP(HttpRequestBase request)
{
string ip = request.Headers["X-Forwarded-For"]; // AWS compatibility
if (string.IsNullOrEmpty(ip))
{
ip = request.UserHostAddress;
}
return ip;
}
}
public class IpController : Controller
{
[HttpGet]
public string Index()
{
return CommonManager.GetIP(Request);
}
}

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.

Example Token Requests

The following example shows how to use the code to generate a token. The initial call from the login page generates the key:

1
$http.get('/api/login?token=' + SecurityManager.generate('user', 'password')).success(function() { ... });

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:

1
SecurityManager.generate('user', 'password');

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.

1
$http.get('/api/person?q=some_query&token=' + SecurityManager.generate()).success(function() { ... });

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.

Conclusion

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 @ GitHub

Download the project source code on GitHub by visiting the project home page.

About the Author

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.

Share