Click here to Skip to main content
14,092,036 members
Click here to Skip to main content
Add your own
alternative version

Tagged as

Stats

148.8K views
59 bookmarked
Posted 3 Aug 2013
Licenced CPOL

Cross Platform Authentication With ASP.NET Web API

, 3 Aug 2013
Rate this:
Please Sign up or sign in to vote.
Cross platform authentication with ASP.NET Web API

It’s becoming increasingly common to expose multiple interfaces for application – many applications have apps for iPhone, Android, Windows mobile in addition to the web interface. Some part of your application functionality may be exposed through other ways like Chrome extension and Windows tile. Depending on the nature of your application, you may potentially expose an API. For giving your users a seamless experience of accessing application from various front-ends, your server needs to have versatile authentication mechanism. Forms authentication is a popular choice for securing ASP.NET and MVC applications. It could be used for securing even the Web API, however there are a couple of issues which makes it less suitable for Web API. Forms Authentication uses cookies and redirection, which doesn’t go well with non-browser clients. Cookies could expose your API to potential Cross-Site Request Forgery(CSRF). Few client libraries do support cookies (i.e. WebClient), however it’s not a good design choice when your services need be accessible from a variety of platforms.

Web API comes with a built-in attribute Authorize (i.e. System.Web.Http.Authorize), which could be used to lockdown your API. Implementation of this filter basically looks at the Thread.CurrentPrincipal to determine whether the user is authenticated and has specified roles. CurrentPrincipal could be set when your authentication mechanism establishes identity of the user. This identity could be accessed from anywhere in the code through Thread.CurrentPrincipal, any new threads would automatically get the identity set on the main thread. As opposed to generic custom filters, Authorize is built-in filter meant for authorization. Authorize filter gets executed before any other filters. You could apply this filter at action, controller level or it could be registered as a global filter. Before we dwell deeper into the Web API processing pipeline, let's take a small detour to see how we could address the sniffing venerability of the HTTP.

Keep the Sniffers at Bay

The fact that HTTP is text-based makes it versatile protocol widely available on virtually every platform. However, the plain-text nature makes it venerable to network sniffing. HTTPS adds a layer on top of HTTP adding security capabilities to it. HTTPS would protect against prying eyes as only the server and client can decrypt the traffic. Enforcing the need to use HTTPS for accessing your API would secure it against Man-In-the-Middle Attack up to great extent. Enabling HTTPS binding in IIS is fairly straight forward. If you need a quick refresher, I have put together the steps along with screenshots in this post – [Enabling HTTPS in IIS]. The next section would discuss a technique for denying the requests not using HTTPS with appropriate message.

Web API Processing Pipeline

Let’s take a quick look at the Web API request processing pipeline to see what is the best place to perform authorization. As you cold see in the image, authorization filters get executed fairly deep in the Web API request processing pipeline, just before your action method. If we could move our security mechanism slightly up in the processing pipeline, we could stop malicious requests there itself and prevent overheads of further processing. Another design advantage of moving the cross-cutting concern like security higher up in the stack is better control and flexibility. Instead of relying on individual developer to add action filter attributes to each controller or action, you could control it at a higher level. You could drive the authorization based on metadata stored in the database or other storage. We could implement custom message handlers (class derived from DelegateHandler) which will allow us to tap into the request (that is HttpRequestMessage) earlier in the request life cycle. We could perform necessary check and return the response from the message handler itself. Let's jump into the code to implement a message handler to ensure that the client is using a secure line (i.e., HTTPS) for connecting to your API.

public class HTTPSGuard:DelegatingHandler
{
    protected override Task<HttpResponseMessage> 
        SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (!request.RequestUri.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
        {
            HttpResponseMessage reply = request.CreateErrorResponse
              (HttpStatusCode.BadRequest, "HTTPS is required for security reason.");
            return Task.FromResult(reply);
        }

        return base.SendAsync(request, cancellationToken);
    }
}

HTTPSGuard inherits from the DelegatingHandler and checks for the HTTPS scheme on the request URI. If the scheme is not HTTPS, it responds with HTTP 400 code (Bad Request) and a message saying HTTPS is required for accessing the API. We need to register message handlers in the WebAPIConfig under App_Start folder as shown below:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MessageHandlers.Add(new HTTPSGuard()); //Global handler - applicable to all the requests
    }
}

This handler will be executed for each incoming request, we are making HTTPS as a requirement for all the operations of the API. Later in the next section, we will register handlers for a specific route. You could use any HTTP tool like Fiddler or HTTP Client to quickly test the API. For testing the API, Chrome extension called Postman is my weapon of choice. It allows to save the requests, which come in handy while testing various security scenarios.

HTTPModule could also accomplish the same goal, however the difference between MessageHandler and HTTPModule is that HTTPModule is IIS specific, whereas MessageHandler is host-agnostic. Message Handlers would work the same while hosting on IIS or self-hosting.

Security Pattern

With HTTPS in place, it's safe to request a user to provide credentials through our front end. But first, let's establish a pattern for our security mechanism. There are various patterns which could be used to address your application specific security requirements. In this post, we will use a token based security pattern. Once the user is successfully authenticated, the server will issue a token which needs to be passed by client with every subsequent request as a proof of user identity.

The security token needs to be strongly encrypted to prevent forgery. The token will be issued and consumed by the same party – our server, so symmetric key encryption like Rijndael/AEC or DES would work fine. However, here I am using public-key cryptography for ease of managing the keys. Common use case of public-key cryptography/x.509 certificates involve sharing your public keys with the collaborating systems which could send data encrypted using your public key, which could be decrypted only using your private key. In our case, we don’t need to and shouldn’t share the public key with any other party as we are the issuer and consumer of the token. So, both keys needs to be protected religiously to prevent forgery of the token.

Another security risk is session hijacking, someone could get hold of the token and could pose as a user. In order to avoid hijacking, we will associate the token to the user machine’s IP address, while validating the token we will be comparing it with the IP address from which the request is being made. Following is the token in pain text, with just two key value pairs separated by comma.

UserId=ninja,IP=192.78.68.90

Following is the class representing the token:

public class Token
{
    public Token(string userId, string fromIP)
    {
        UserId = userId;
        IP = fromIP;
    }

    public string UserId { get; private set; }
    public string IP { get; private set; }

    public string Encrypt()
    {
        CryptographyHelper cryptographyHelper = new CryptographyHelper();
        X509Certificate2 certificate = cryptographyHelper.GetX509Certificate("CN=WebAPI-Token");
        return cryptographyHelper.Encrypt(certificate, this.ToString());
    }

    public override string ToString()
    {
        return String.Format("UserId={0};IP={1}", this.UserId, this.IP);
    }

    public static Token Decrypt(string encryptedToken)
    {
        CryptographyHelper cryptographyHelper = new CryptographyHelper();
        X509Certificate2 certificate = cryptographyHelper.GetX509Certificate("CN=WebAPI-Token");
        string decrypted = cryptographyHelper.Decrypt(certificate, encryptedToken);

        //Splitting it to dictionary
        Dictionary<string, string> dictionary = decrypted.ToDictionary();
        return new Token(dictionary["UserId"], dictionary["IP"]);
    }
}

Create the x.509 certificate by running the following command on the Visual Studio command prompt while running as administrator:

makecert -sr LocalMachine -ss My sha1 -n CN=WebAPI-Token -sk y exchange -pe

CryptographyHelper provides helper methods for encryption and decryption, the code could be found here .

Put the Guard in Place

We have most of the pieces in place, so let's create a message handler which will check for token in the header of every incoming request.

public class TokenInspector : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync
         (HttpRequestMessage request, CancellationToken cancellationToken)
    {
        const string TOKEN_NAME = "X-Token";

        if (request.Headers.Contains(TOKEN_NAME))
        {
            string encryptedToken = request.Headers.GetValues(TOKEN_NAME).First();
            try
            {
                Token token = Token.Decrypt(encryptedToken);
                bool isValidUserId = IdentityStore.IsValidUserId(token.UserId);
                bool requestIPMatchesTokenIP = token.IP.Equals(request.GetClientIP());

                if (!isValidUserId || !requestIPMatchesTokenIP)
                {
                    HttpResponseMessage reply = request.CreateErrorResponse
                        (HttpStatusCode.Unauthorized, "Invalid identity or client machine.");
                    return Task.FromResult(reply);
                }
            }
            catch (Exception ex)
            {
                HttpResponseMessage reply = request.CreateErrorResponse
                              (HttpStatusCode.Unauthorized, "Invalid token.");
                return Task.FromResult(reply);
            }
        }
        else
        {
            HttpResponseMessage reply = request.CreateErrorResponse
              (HttpStatusCode.Unauthorized, "Request is missing authorization token.");
            return Task.FromResult(reply);
        }

        return base.SendAsync(request, cancellationToken);
    }
}

X-Token is the HTTP header in which we expect the client to supply the token issued after authentication. Any request which doesn’t contain token is refused politely with HTTP 401 code. If the token is supplied, it is decrypted, user id is checked for validity against identity store. The IP address in the token is matched with the IP address of the request to prevent session hijacking. We need to register this message handler in our API config class as shown below. UsersController provides operations for authenticating the user, so of course we can’t demand token on that request, so that is excluded by explicitly specifying the route.

public static void Register(HttpConfiguration config)
{
    //Create and instance of TokenInspector setting the default inner handler
    TokenInspector tokenInspector = new TokenInspector() 
         { InnerHandler = new HttpControllerDispatcher(config) };

  //Just exclude the users controllers from need to provide valid token, so they could authenticate
    config.Routes.MapHttpRoute(
        name: "Authentication",
        routeTemplate: "api/users/{id}",
        defaults: new { controller = "users" }
    );

    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional },
        constraints: null,
        handler: tokenInspector
    );

    config.MessageHandlers.Add(new HTTPSGuard()); //Global handler - applicable to all the requests
}

You could download the code and play with it using any HTTP client to see it in action. The code along with unit tests could be found at https://github.com/patelsan/WebAPIAuthentication.

I hope this was useful. I would love to hear your thoughts on this.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

patelsan
Web Developer
United States United States
No Biography provided

You may also be interested in...

Comments and Discussions

 
QuestionGreat article ! Pin
Praneet Nadkar10-May-16 22:35
memberPraneet Nadkar10-May-16 22:35 
Questiondynamic ip address on mobile device Pin
fulvio22-Oct-15 5:58
memberfulvio22-Oct-15 5:58 
QuestionCryptographic Error Pin
namlas_9520-Apr-15 1:41
groupnamlas_9520-Apr-15 1:41 
QuestionGreat Article!!!!! Pin
Member 251322126-Feb-15 16:19
memberMember 251322126-Feb-15 16:19 
QuestionHow to use? Pin
ThePhat3228-Jan-15 5:03
memberThePhat3228-Jan-15 5:03 
QuestionGreat Article ! Pin
Prasanna Devarajan22-Jul-14 1:20
memberPrasanna Devarajan22-Jul-14 1:20 
QuestionCredential? Pin
Rahman Mahmoodi10-Mar-14 1:12
memberRahman Mahmoodi10-Mar-14 1:12 
QuestionHow i Create The Tokens ? Pin
asche7718-Feb-14 14:30
memberasche7718-Feb-14 14:30 
QuestionA good starter for a 4 Pin
TeaTime27-Jan-14 3:52
memberTeaTime27-Jan-14 3:52 
Questionlimitations in Azure & CloudApp? - (RSACryptoServiceProvider)certificate.PrivateKey Pin
Saila On21-Jan-14 0:03
memberSaila On21-Jan-14 0:03 
QuestionAuthentication for specific actions or controllers Pin
c#200919-Dec-13 21:38
memberc#200919-Dec-13 21:38 
GeneralMy vote of 5 Pin
Quentin in SA18-Dec-13 7:49
memberQuentin in SA18-Dec-13 7:49 
QuestionCorrect command to create Certificate Pin
ffernandez2325-Nov-13 8:43
memberffernandez2325-Nov-13 8:43 
QuestionCool Pin
the headlessnick14-Nov-13 4:21
memberthe headlessnick14-Nov-13 4:21 
QuestionHow Token is Persisted? Pin
Member 14151005-Nov-13 5:16
memberMember 14151005-Nov-13 5:16 
AnswerRe: How Token is Persisted? Pin
Member 14151005-Nov-13 9:27
memberMember 14151005-Nov-13 9:27 
QuestionWishList asks for token again Pin
Rahul.PP14-Oct-13 22:44
memberRahul.PP14-Oct-13 22:44 
AnswerRe: WishList asks for token again Pin
Quentin in SA18-Dec-13 7:51
memberQuentin in SA18-Dec-13 7:51 
SuggestionGreat article. You might consider using WebAPI Token Auth NuGet Package Pin
dgandalf6-Oct-13 13:58
memberdgandalf6-Oct-13 13:58 
Questioncertificate.PrivateKey' threw an exception of type 'System.Security.Cryptography.CryptographicException Pin
kristaffa1-Oct-13 0:34
memberkristaffa1-Oct-13 0:34 
AnswerRe: certificate.PrivateKey' threw an exception of type 'System.Security.Cryptography.CryptographicException Pin
Chris Chou20-Oct-13 8:09
memberChris Chou20-Oct-13 8:09 
GeneralRe: certificate.PrivateKey' threw an exception of type 'System.Security.Cryptography.CryptographicException Pin
comar200722-Nov-13 6:33
membercomar200722-Nov-13 6:33 
QuestionGreat work! Pin
kristaffa30-Sep-13 1:35
memberkristaffa30-Sep-13 1:35 
QuestionHTTPS Pin
ARIEL TADEU ANDRADE DE MORAES26-Sep-13 11:45
memberARIEL TADEU ANDRADE DE MORAES26-Sep-13 11:45 
Questiondecrypted.ToDictionary() Pin
Olli Müller23-Sep-13 23:00
professionalOlli Müller23-Sep-13 23:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01 | 2.8.190518.1 | Last Updated 3 Aug 2013
Article Copyright 2013 by patelsan
Everything else Copyright © CodeProject, 1999-2019
Layout: fixed | fluid