How to secure an ASP.NET Web API

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?


    Update:

    I have put my another answer how to use JWT authentication for Web API in here for anyone interested in JWT:

    JWT Authentication for Asp.Net Web Api


    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:

  • Timestamp: time that request is sent (UTC or GMT)
  • HTTP verb: GET, POST, PUT, DELETE.
  • post data and query string,
  • 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:

    GETn
    Thursday, August 02, 2012 3:30:32 PMn
    /webapi.hmac/api/valuesn
    

    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

    GETn
    Thursday, August 02, 2012 3:30:32 PMn
    /webapi.hmac/api/valuesn
    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


    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/


    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.

    链接地址: http://www.djcxy.com/p/20514.html

    上一篇: 推荐的ServiceStack API结构

    下一篇: 如何保护ASP.NET Web API