How to secure an ASP.NET Web API

307,035

Solution 1

Update:

I have added this link to my other answer how to use JWT authentication for ASP.NET Web API here for anyone interested in JWT.


We have managed to apply HMAC authentication to secure Web API, and it worked okay. HMAC authentication uses a secret key for each consumer which both consumer and server both know to hmac hash a message, HMAC256 should be used. Most of the cases, hashed password of the consumer is used as a secret key.

The message normally is built from data in the HTTP request, or even customized data which is added to HTTP header, the message might include:

  1. Timestamp: time that request is sent (UTC or GMT)
  2. HTTP verb: GET, POST, PUT, DELETE.
  3. post data and query string,
  4. URL

Under the hood, HMAC authentication would be:

Consumer sends a HTTP request to web server, after building the signature (output of hmac hash), the template of HTTP request:

User-Agent: {agent}   
Host: {host}   
Timestamp: {timestamp}
Authentication: {username}:{signature}

Example for GET request:

GET /webapi.hmac/api/values

User-Agent: Fiddler    
Host: localhost    
Timestamp: Thursday, August 02, 2012 3:30:32 PM 
Authentication: cuongle:LohrhqqoDy6PhLrHAXi7dUVACyJZilQtlDzNbLqzXlw=

The message to hash to get signature:

GET\n
Thursday, August 02, 2012 3:30:32 PM\n
/webapi.hmac/api/values\n

Example for POST request with query string (signature below is not correct, just an example)

POST /webapi.hmac/api/values?key2=value2

User-Agent: Fiddler    
Host: localhost    
Content-Type: application/x-www-form-urlencoded
Timestamp: Thursday, August 02, 2012 3:30:32 PM 
Authentication: cuongle:LohrhqqoDy6PhLrHAXi7dUVACyJZilQtlDzNbLqzXlw=

key1=value1&key3=value3

The message to hash to get signature

GET\n
Thursday, August 02, 2012 3:30:32 PM\n
/webapi.hmac/api/values\n
key1=value1&key2=value2&key3=value3

Please note that form data and query string should be in order, so the code on the server get query string and form data to build the correct message.

When HTTP request comes to the server, an authentication action filter is implemented to parse the request to get information: HTTP verb, timestamp, uri, form data and query string, then based on these to build signature (use hmac hash) with the secret key (hashed password) on the server.

The secret key is got from the database with the username on the request.

Then server code compares the signature on the request with the signature built; if equal, authentication is passed, otherwise, it failed.

The code to build signature:

private static string ComputeHash(string hashedPassword, string message)
{
    var key = Encoding.UTF8.GetBytes(hashedPassword.ToUpper());
    string hashString;

    using (var hmac = new HMACSHA256(key))
    {
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
        hashString = Convert.ToBase64String(hash);
    }

    return hashString;
}

So, how to prevent replay attack?

Add constraint for the timestamp, something like:

servertime - X minutes|seconds  <= timestamp <= servertime + X minutes|seconds 

(servertime: time of request coming to server)

And, cache the signature of the request in memory (use MemoryCache, should keep in the limit of time). If the next request comes with the same signature with the previous request, it will be rejected.

The demo code is put as here: https://github.com/cuongle/Hmac.WebApi

Solution 2

I would suggest starting with the most straightforward solutions first - maybe simple HTTP Basic Authentication + HTTPS is enough in your scenario.

If not (for example you cannot use https, or need more complex key management), you may have a look at HMAC-based solutions as suggested by others. A good example of such API would be Amazon S3 (http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html)

I wrote a blog post about HMAC based authentication in ASP.NET Web API. It discusses both Web API service and Web API client and the code is available on bitbucket. http://www.piotrwalat.net/hmac-authentication-in-asp-net-web-api/

Here is a post about Basic Authentication in Web API: http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers/

Remember that if you are going to provide an API to 3rd parties, you will also most likely be responsible for delivering client libraries. Basic authentication has a significant advantage here as it is supported on most programming platforms out of the box. HMAC, on the other hand, is not that standardized and will require custom implementation. These should be relatively straightforward but still require work.

PS. There is also an option to use HTTPS + certificates. http://www.piotrwalat.net/client-certificate-authentication-in-asp-net-web-api-and-windows-store-apps/

Solution 3

Have you tried DevDefined.OAuth?

I have used it to secure my WebApi with 2-Legged OAuth. I have also successfully tested it with PHP clients.

It's quite easy to add support for OAuth using this library. Here's how you can implement the provider for ASP.NET MVC Web API:

1) Get the source code of DevDefined.OAuth: https://github.com/bittercoder/DevDefined.OAuth - the newest version allows for OAuthContextBuilder extensibility.

2) Build the library and reference it in your Web API project.

3) Create a custom context builder to support building a context from HttpRequestMessage:

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using System.Web;

using DevDefined.OAuth.Framework;

public class WebApiOAuthContextBuilder : OAuthContextBuilder
{
    public WebApiOAuthContextBuilder()
        : base(UriAdjuster)
    {
    }

    public IOAuthContext FromHttpRequest(HttpRequestMessage request)
    {
        var context = new OAuthContext
            {
                RawUri = this.CleanUri(request.RequestUri), 
                Cookies = this.CollectCookies(request), 
                Headers = ExtractHeaders(request), 
                RequestMethod = request.Method.ToString(), 
                QueryParameters = request.GetQueryNameValuePairs()
                    .ToNameValueCollection(), 
            };

        if (request.Content != null)
        {
            var contentResult = request.Content.ReadAsByteArrayAsync();
            context.RawContent = contentResult.Result;

            try
            {
                // the following line can result in a NullReferenceException
                var contentType = 
                    request.Content.Headers.ContentType.MediaType;
                context.RawContentType = contentType;

                if (contentType.ToLower()
                    .Contains("application/x-www-form-urlencoded"))
                {
                    var stringContentResult = request.Content
                        .ReadAsStringAsync();
                    context.FormEncodedParameters = 
                        HttpUtility.ParseQueryString(stringContentResult.Result);
                }
            }
            catch (NullReferenceException)
            {
            }
        }

        this.ParseAuthorizationHeader(context.Headers, context);

        return context;
    }

    protected static NameValueCollection ExtractHeaders(
        HttpRequestMessage request)
    {
        var result = new NameValueCollection();

        foreach (var header in request.Headers)
        {
            var values = header.Value.ToArray();
            var value = string.Empty;

            if (values.Length > 0)
            {
                value = values[0];
            }

            result.Add(header.Key, value);
        }

        return result;
    }

    protected NameValueCollection CollectCookies(
        HttpRequestMessage request)
    {
        IEnumerable<string> values;

        if (!request.Headers.TryGetValues("Set-Cookie", out values))
        {
            return new NameValueCollection();
        }

        var header = values.FirstOrDefault();

        return this.CollectCookiesFromHeaderString(header);
    }

    /// <summary>
    /// Adjust the URI to match the RFC specification (no query string!!).
    /// </summary>
    /// <param name="uri">
    /// The original URI. 
    /// </param>
    /// <returns>
    /// The adjusted URI. 
    /// </returns>
    private static Uri UriAdjuster(Uri uri)
    {
        return
            new Uri(
                string.Format(
                    "{0}://{1}{2}{3}", 
                    uri.Scheme, 
                    uri.Host, 
                    uri.IsDefaultPort ?
                        string.Empty :
                        string.Format(":{0}", uri.Port), 
                    uri.AbsolutePath));
    }
}

4) Use this tutorial for creating an OAuth provider: http://code.google.com/p/devdefined-tools/wiki/OAuthProvider. In the last step (Accessing Protected Resource Example) you can use this code in your AuthorizationFilterAttribute attribute:

public override void OnAuthorization(HttpActionContext actionContext)
{
    // the only change I made is use the custom context builder from step 3:
    OAuthContext context = 
        new WebApiOAuthContextBuilder().FromHttpRequest(actionContext.Request);

    try
    {
        provider.AccessProtectedResourceRequest(context);

        // do nothing here
    }
    catch (OAuthException authEx)
    {
        // the OAuthException's Report property is of the type "OAuthProblemReport", it's ToString()
        // implementation is overloaded to return a problem report string as per
        // the error reporting OAuth extension: http://wiki.oauth.net/ProblemReporting
        actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
            {
               RequestMessage = request, ReasonPhrase = authEx.Report.ToString()
            };
    }
}

I have implemented my own provider so I haven't tested the above code (except of course the WebApiOAuthContextBuilder which I'm using in my provider) but it should work fine.

Solution 4

Web API introduced an Attribute [Authorize] to provide security. This can be set globally (global.asx)

public static void Register(HttpConfiguration config)
{
    config.Filters.Add(new AuthorizeAttribute());
}

Or per controller:

[Authorize]
public class ValuesController : ApiController{
...

Of course your type of authentication may vary and you may want to perform your own authentication, when this occurs you may find useful inheriting from Authorizate Attribute and extending it to meet your requirements:

public class DemoAuthorizeAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        if (Authorize(actionContext))
        {
            return;
        }
        HandleUnauthorizedRequest(actionContext);
    }

    protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        var challengeMessage = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
        challengeMessage.Headers.Add("WWW-Authenticate", "Basic");
        throw new HttpResponseException(challengeMessage);
    }

    private bool Authorize(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        try
        {
            var someCode = (from h in actionContext.Request.Headers where h.Key == "demo" select h.Value.First()).FirstOrDefault();
            return someCode == "myCode";
        }
        catch (Exception)
        {
            return false;
        }
    }
}

And in your controller:

[DemoAuthorize]
public class ValuesController : ApiController{

Here is a link on other custom implemenation for WebApi Authorizations:

http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-membership-provider/

Solution 5

If you want to secure your API in a server to server fashion (no redirection to website for 2 legged authentication). You can look at OAuth2 Client Credentials Grant protocol.

https://dev.twitter.com/docs/auth/application-only-auth

I have developed a library that can help you easily add this kind of support to your WebAPI. You can install it as a NuGet package:

https://nuget.org/packages/OAuth2ClientCredentialsGrant/1.0.0.0

The library targets .NET Framework 4.5.

Once you add the package to your project, it will create a readme file in the root of your project. You can look at that readme file to see how to configure/use this package.

Cheers!

Share:
307,035
Tushee
Author by

Tushee

Full-stack Software Architect and Developer in C#, .NET, Javascript, Typescript, AngularJS, Angular .NET, JADE, related web technologies. SOreadytohelp

Updated on June 20, 2020

Comments

  • Tushee
    Tushee almost 4 years

    I want to build a RESTful web service using ASP.NET Web API that third-party developers will use to access my application's data.

    I've read quite a lot about OAuth and it seems to be the standard, but finding a good sample with documentation explaining how it works (and that actually does work!) seems to be incredibly difficult (especially for a newbie to OAuth).

    Is there a sample that actually builds and works and shows how to implement this?

    I've downloaded numerous samples:

    • DotNetOAuth - documentation is hopeless from a newbie perspective
    • Thinktecture - can't get it to build

    I've also looked at blogs suggesting a simple token-based scheme (like this) - this seems like re-inventing the wheel but it does have the advantage of being conceptually fairly simple.

    It seems there are many questions like this on SO but no good answers.

    What is everybody doing in this space?

  • Tushee
    Tushee over 11 years
    Thanks, that's useful code to look at. Did you have some client code too?
  • cuongle
    cuongle over 11 years
    You can use filter for testing or write a HttpClient to create request.
  • Tushee
    Tushee over 11 years
    Thanks - I'll take a look at this, though for now I've rolled my own HMAC-based solution.
  • James
    James over 11 years
    @CuongLe In your AuthenticateAttribute on GitHub you are validating the timestamp AND checking if the previous signature was in the memory cache for preventing replay attacks. Is it necessary for both checks to happen? Wouldn't it be more reliable to just check the timestamp manually (your first suggestion)?
  • cuongle
    cuongle over 11 years
    @James: only timestamp seems not sufficient much, during short of time they may simulate the request and sent to server, I have just edited my post, use both would be the best.
  • sambomartin
    sambomartin over 11 years
    @CraigShearer - hi, you say you've rolled your own.. just had a few questions if you don't mind sharing. I'm in a similar position, where i have a relatively small MVC Web API. The API controllers sit alongside other controller/actions which are under forms auth. Implementing OAuth seems an overkill when I already have a membership provider i could use and I only need to secure a handful of operations. I really want an auth action that returns an encrypted token - then used the token in subsequent calls? any info welcome before I commit to implementing an existing auth solution. thanks!
  • mono blaine
    mono blaine about 11 years
    @CuongLe Using the user's hashed password as a secret key to sign requests is a good idea. At least you will make sure that the secret key remains secret and will never be transmitted over the wire. However, hashing the password needs a salt, and normally that salt should be random. In your case, this salt should be the same in both the client and the server. Also, it means that all users' passwords are hashed using the same salt. Isn't that bad? Or am I completely wrong?
  • Filip Stas
    Filip Stas about 11 years
    Are you sure this is working as it should? you are hashing the timestamp with the message and caching that message. This would mean a different signature each request which would render your cached signature useless.
  • cuongle
    cuongle about 11 years
    @FilipStas: seems I don't get your point, the reason to use Cache in here is to prevent relay attack, nothing more
  • Pravesh Singh
    Pravesh Singh almost 11 years
    Please explain how to add authenticated user and make to allow AllowAnonymous attribute is working, currently it is not working.
  • ChrisO
    ChrisO almost 11 years
    Great answer. Could you show how a user could generate their signature? I'm stuck at this part.
  • cuongle
    cuongle almost 11 years
    @ChrisO: You can refer [this page] (jokecamp.wordpress.com/2012/10/21/…). I will update this source soon
  • matt.
    matt. almost 11 years
    @CuongLe How does the consumer get the secret? In your post you state that the hashed password is typically what is used but how would that work where the password is salted and hashed then stored in a database? Surely the consumer can't just requested the password? What am I missing here?
  • cuongle
    cuongle almost 11 years
    @nwdev: It's like how you get the password on website, by registering or sending over the email
  • jlrolin
    jlrolin over 10 years
    @Maksymilian Majer - Any chance you can share how you've implemented the provider in more detail? I'm having some problems sending responses back to the client.
  • Bargitta
    Bargitta about 10 years
    @CuongLe Do you use this to authenticate the app using your API, or authenticate a real user?
  • cuongle
    cuongle about 10 years
    @Bargitta: What is the point in your question?
  • Bargitta
    Bargitta about 10 years
    @CuongLe I have posted my question here stackoverflow.com/questions/22630447/…
  • sacretruth
    sacretruth about 10 years
    I really like this solution, however how do you prevent an attacker from prying into the client source and retrieving the private key in order to use this to build the signature ??
  • Cosmin
    Cosmin almost 10 years
    I don't know if I understood this ok. So, when you are consuming that API, you need to put something in QueryString like following url : localhost/webapi.hmac/api/…" ? You parse that query string in OnActionExecuting and set specific headers?
  • Toan Nguyen
    Toan Nguyen over 9 years
    @Kosmo The values (userName:signature) are added to the Authorization header
  • Toan Nguyen
    Toan Nguyen over 9 years
    @CuongLe Hi Cuong, so in the context of an MVC web application, after users have logged in (authenticated), did you send back the hashed password as an hidden field in the form? Or how can you get the secret key to sign the message on the client side?
  • cuongle
    cuongle over 9 years
    @kooldave98: Good question, I guess we don't hardcode the password in javascript file, instead it should be retrieved from database. Also, js files should be minified.
  • cuongle
    cuongle over 9 years
    @Kosmo: It puts on HTTP request header
  • cuongle
    cuongle over 9 years
    @ToanNguyen: seems I am not sure I completely understand your question, if your application is ASP.NET MVC you should use built-in form authentication. You can store secret key (password) for each user in database
  • Toan Nguyen
    Toan Nguyen over 9 years
    @CuongLe My question was how can you calculate the based64 value on the client side so that you would be able to compare that value with the one from the server, since you need the secret key, in this case it is the hash value of a user's password? So I guess when you rendered the page, you included the hash value into a hidden field?
  • cuongle
    cuongle over 9 years
    @ToanNguyen: on Javascript, you can use cryptojs at: code.google.com/p/crypto-js to calculate hash. Hidden field will store value on body of request, not the header. In this case, you should call api by ajax instead letting the form submits directly to server
  • Toan Nguyen
    Toan Nguyen over 9 years
    @CuongLe So it means you have to push the "secret" in the form you render from the server. Which means that a malicious user will be able to get it to conduct the signature of the message? How do you ensure that data is not submitted by a malicious user?
  • sacretruth
    sacretruth over 9 years
    @ToanNguyen that is exactly what I need to know too. I still cannot understand how to prevent an attacker from getting the secret !!!
  • sacretruth
    sacretruth over 9 years
    @CuongLe: okay, after retrieving the secret from the database, how can you then prevent an attacker from getting this retrieved secret ??
  • cuongle
    cuongle over 9 years
    @ToanNguyen: well I now understand what you both meant, your questions are for HMAC authentication in general, not specific for ASP.NET Web API, so in the context of client side, we need to trust user. You can put the secret key anywhere that "anonymous" user cannot access, Js files, or even on html page as long as the secret key is NOT brought over by HTTP request. If you put secret key on the form only for authenticated user, how can malicious user access that form?
  • cuongle
    cuongle over 9 years
    @kooldave98: please see my previous comment
  • barrypicker
    barrypicker over 9 years
    Are you sharing/providing source code for this framework as open source?
  • Amanda
    Amanda about 9 years
    Thanks for the example @Dalorzo, but I have some issues. I looked at the attached link, but following that instructions doesn't quite work. I also found needed info missing. Firstly, when I create the new project, is it right to choose Individual User Accounts for authentication? Or do I leave it at no authentication. I'm also not getting the mentioned 302 error, but am getting a 401 error. Lastly, how do I pass the needed info from my view to the controller? What must my ajax call look like? Btw, I'm using forms authentication for my MVC views. Is that a problem?
  • refactor
    refactor over 8 years
    My simple approach would be to pass User ID/PWD in the custome HTTP request header and to implement HTTPS/SSL for the WEB API
  • refactor
    refactor over 8 years
    The solution suggested works , but you can't prevent Man-in-the-Middle attack , for that you have to implement HTTPS
  • Pon Saravanan
    Pon Saravanan over 8 years
    It is working fantastically. Just nice to learn and start working on our own access tokens.
  • corentinaltepe
    corentinaltepe over 7 years
    Is this an implementation of OAuth?
  • Dragon
    Dragon about 7 years
    @CuongLe what about csrf and xss attack in your apporach,a hidden secret in form will not prevent these attacks?
  • Vitaliy Markitanov
    Vitaliy Markitanov over 5 years
    One small comment - be careful with AuthorizeAttribute, as there are two different classes with same name, in different namespaces: 1. System.Web.Mvc.AuthorizeAttribute -> for MVC controllers 2. System.Web.Http.AuthorizeAttribute - > for WebApi.
  • abdul qayyum
    abdul qayyum over 4 years
    JFR: First Link is Broken and NuGet package was never updated
  • Mehdi Dehghani
    Mehdi Dehghani over 4 years
    @cuongle how to use this solution without authentication? there is 2 notes: 1. what would be alternative to password? 2. in one of your comments, you wrote If you put secret key on the form only for authenticated user, how can malicious user access that form, without authentication what is the answer to that question?