10

I am trying to make a secure asp.net web api. For that I have followed the below link

MessageHandler for token

So now each and every api request needs a token which I am supplying in the request header as below for example

public class TestController : Controller
{

    public string GetProducts()
    {
        Uri myUri = new Uri("http://localhost:420420/api/products");
        WebRequest myWebRequest = WebRequest.Create(myUri);

        myWebRequest.Method = "GET";
        myWebRequest.ContentType = "application/json";
        myWebRequest.Headers.Add("Authorization-Token", RSAClass.accessToken);

        using (WebResponse response = myWebRequest.GetResponse())
        {
            using (var responseStream = response.GetResponseStream())
            {
                var reader = new StreamReader(responseStream);
                return reader.ReadToEnd();
            }
        }
    }    
  }

So I am now able to make each and every api request, check for a token in the header. But how do I accomplish authorization, I mean how can I not allow this token not access some actions in the same controller.I just need an idea.Hope I explained well enough.

Edit:

public class TestController : Controller
{
    public string GetProducts()
    {
        Uri myUri = new Uri("http://localhost:420420/api/products");         

        WebRequest myWebRequest = WebRequest.Create(myUri);

        myWebRequest.Method = "GET";
        myWebRequest.ContentType = "application/json";
        myWebRequest.Headers.Add("Authorization-Token", RSAClass.accessToken);

        **using (WebResponse response = myWebRequest.GetResponse())
        {
            using (var responseStream = response.GetResponseStream())
            {
                var reader = new StreamReader(responseStream);
                return reader.ReadToEnd();
            }
        }**
 }

I am making a request to the "api" controller, inside above controller, using webrequest(I will change it later to HttpClient). In the code between ** ** above I am getting 404 page not found for myWebRequest.GetResponse()

Below is my api controller

public class ProductsController : ApiController
{

    TestModelContainer testModel = new TestModelContainer();

    [Authorize(Roles="Users")]
    public IEnumerable<Products> GetProducts()
    {
        IEnumerable<Products> products = (from prods in testModel.Products
                        select prods);
        return products;        
    }        
 }
}

Now in the delegating handler I have the following code

public class TokenValidationHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
     CancellationToken cancellationToken)
    {
        TestModelContainer testModel = new TestModelContainer();

        var token = "";
        try
        {

            if (request.Headers.Contains("Authorization-Token"))
            {

                token = request.Headers.GetValues("Authorization-Token").FirstOrDefault();

                if (String.IsNullOrEmpty(token))
                {
                    return Task<HttpResponseMessage>.Factory.StartNew(() =>
                    {
                        return new HttpResponseMessage(HttpStatusCode.BadRequest)
                        {
                            Content = new StringContent("Missing Authorization-Token")
                        };
                    });
                }
            }
            else
            {
                return Task<HttpResponseMessage>.Factory.StartNew(() =>
                {
                    return new HttpResponseMessage(HttpStatusCode.BadRequest)
                    {
                        Content = new StringContent("You need to include Authorization-Token " +
                        "header in your request")
                    };
                });
            }


            var decryptedToken = RSAClass.Decrypt(token);
            var foundUser =  (from user in testModel.Users
                                where user.Name == decryptedToken
                                select user).Any();              

            if (!foundUser)
                return Task<HttpResponseMessage>.Factory.StartNew(() =>
                {
                    return new HttpResponseMessage(HttpStatusCode.Forbidden)
                    {
                        Content = new StringContent("Unauthorized User")
                    };
                });

      var identity = new GenericIdentity(decryptedToken);
              string[] roles = new string[] { "Users", "Testers" };

              var principal = new GenericPrincipal(identity, roles);
              Thread.CurrentPrincipal = principal;
        }
        catch (Exception ex)
        {
            return Task<HttpResponseMessage>.Factory.StartNew(() =>
            {
                return new HttpResponseMessage(HttpStatusCode.InternalServerError)
                {
                    Content = new StringContent("Error encountered while attempting to process authorization token")
                };
            });
        }
        return base.SendAsync(request, cancellationToken);
    }

The 404 error doesnt rise if i remove the Authorize attribute from the api controller, and then I am able to access it.

Update(I believe solution too):

this is how the issue got solved

I have changed the TestController method as below suggested by Darin Dimitrov

public class TestsController : Controller
{
    public ActionResult GetProducts()
    {
        var productsUrl = Url.RouteUrl("DefaultApi", new { httproute = "", controller = "products" }, "http");
        using (var client = new HttpClient())
        {
            client.DefaultRequestHeaders.Add("Authorization-Token", RSAClass.accessToken);

         var products = client
               .GetAsync(productsUrl)
                 .Result;

            if (products.StatusCode == HttpStatusCode.Unauthorized)
            {
                return Content("Sorry you are not authorized to perform this operation");
            }

            var prods = products.Content
                .ReadAsAsync<IEnumerable<Products>>()
                .Result;

            return Json(prods, JsonRequestBehavior.AllowGet);
        }
    }

The issue was I didnt know how to make a call to the api, thanks to Darin for his great support(he was very quick too).

Thanks

CrazyNooB
  • 1,553
  • 3
  • 17
  • 23

1 Answers1

26

You register the handler in Global.asax:

GlobalConfiguration
    .Configuration
    .MessageHandlers
    .Add(new TokenValidationHandler());

and then decorate controllers/actions that require authorization with the [Authorize] attribute:

public class MyController : ApiController
{
    [Authorize]
    public string Get(string id)
    {
        ...          
    }
}

For role based authorization you may could take a look at the following example: https://stackoverflow.com/a/11536349/29407

It uses basic authentication over SSL and relies on the built-in membership and role providers.


UPDATE:

According to the numerous comments left I get the impression that my answer was not clear enough. Let me elaborate.

  1. Create a new ASP.NET MVC 4 project using the Empty Template
  2. Define a model:

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    
  3. Define an ApiController:

    public class ProductsController : ApiController
    {
        // GET /api/products => only users having the Users role can call this
        [Authorize(Roles = "Users")]
        public HttpResponseMessage Get()
        {
            var products = Enumerable.Range(1, 5).Select(x => new Product
            {
                Id = x,
                Name = "product " + x
            });
            return Request.CreateResponse(HttpStatusCode.OK, products);
        }
    
        // GET /api/products => only users having the Admin role can call this
        [Authorize(Roles = "Admin")]
        public void Post(Product product)
        {
        }
    }
    
  4. Define a RSAHelper:

    public class RSAClass
    {
        private static string _privateKey = "<RSAKeyValue><Modulus>poQS/c9tLkgg84xYZpnUBHP6fy24D6XmzhQ8yCOG317hfUNhRt6Z9N4oTn+QcOTh/DAnul4Q901GrHbPrMB8tl1LtbpKbvGftPhyR7OLQVnWC1Oz10t2tHEo7mqyPyAVuYsq8Q1E3YNTh2V6+PRvMiAWGUHGyyG7fKjt/R9W+RE=</Modulus><Exponent>AQAB</Exponent><P>4G09wYejA4iLakpAcjXbE/zV9tXTNsYqVIWeXF4hzwMmwmin7ru/WQzXu2DdapXXOJIKqrkfzXlcPwCsW5b9rQ==</P><Q>vfEq13Et+cP4eGgsR+crDQH0Mi+G6UW5ACfuDs/zam1o+CE70pLgeWawfqW4jRN30/VHDnTF9DZuotH6zihNdQ==</Q><DP>JoZaHYidERQ1am+IlJJuIwY57H9UHIjz50JwpsZ540FVO/YfLboI5M5xkfbUy2EhatKXBit1LB5zGVWSQL6wmQ==</DP><DQ>Gxk7KX2GN6oT2unR13hNlg9/TWGmd8VwvWr09bwJWFe/sBbduA8oY2mZKJhwGgB7CgxmVNOoIk1Zv3UBuUPauQ==</DQ><InverseQ>ZwJpSUZ09lCfiCF3ILB6F1q+6NC5hFH0O4924X9B4LZ8G4PRuudBMu1Yg0WNROUqVi3zfihKvzHnquHshSL56A==</InverseQ><D>pPQNRDVpeQGm8t1C7VDRwR+LNNV7krTMMbXGiJT5FOoPAmHvSZ9WcEZrM2gXFF8IpySlFm/86p84tbx0+jMs1niU52VsTscsamGbTzbsxeoHAt1fQUvzYveOGoRezotXblboVB2971r6avMHNtAk0FAdjvh4TjGZJCGTqNHD0mE=</D></RSAKeyValue>";
        private static string _publicKey = "<RSAKeyValue><Modulus>poQS/c9tLkgg84xYZpnUBHP6fy24D6XmzhQ8yCOG317hfUNhRt6Z9N4oTn+QcOTh/DAnul4Q901GrHbPrMB8tl1LtbpKbvGftPhyR7OLQVnWC1Oz10t2tHEo7mqyPyAVuYsq8Q1E3YNTh2V6+PRvMiAWGUHGyyG7fKjt/R9W+RE=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>";
        private static UnicodeEncoding _encoder = new UnicodeEncoding();
    
        public static string Decrypt(string data)
        {
            try
            {
                var rsa = new RSACryptoServiceProvider();
                var dataArray = data.Split(new char[] { ',' });
    
                byte[] dataByte = new byte[dataArray.Length];
                for (int i = 0; i < dataArray.Length; i++)
                {
                    dataByte[i] = Convert.ToByte(dataArray[i]);
                }
    
                rsa.FromXmlString(_privateKey);
                var decryptedByte = rsa.Decrypt(dataByte, false);
                return _encoder.GetString(decryptedByte);
            }
            catch (Exception)
            {
                throw new RSAException();
            }
        }
    
        public static string Encrypt(string data)
        {
            try
            {
                var rsa = new RSACryptoServiceProvider();
                rsa.FromXmlString(_publicKey);
                var dataToEncrypt = _encoder.GetBytes(data);
                var encryptedByteArray = rsa.Encrypt(dataToEncrypt, false).ToArray();
                var length = encryptedByteArray.Count();
                var item = 0;
                var sb = new StringBuilder();
                foreach (var x in encryptedByteArray)
                {
                    item++;
                    sb.Append(x);
    
                    if (item < length)
                        sb.Append(",");
                }
    
                return sb.ToString();
    
            }
            catch (Exception ex)
            {
                throw new RSAException();
            }
        }
    
        public class RSAException : Exception
        {
            public RSAException() : base("RSA Encryption Error") { }
        }
    }
    
  5. Define a TokenValidationHandler:

    public class TokenValidationHandler : DelegatingHandler
    {
        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            try
            {
                if (!request.Headers.Contains("Authorization-Token"))
                {
                    return Task<HttpResponseMessage>.Factory.StartNew(() =>
                    {
                        return new HttpResponseMessage(HttpStatusCode.BadRequest)
                        {
                            Content = new StringContent("You need to include Authorization-Token header in your request")
                        };
                    });
                }
    
                var token = request.Headers.GetValues("Authorization-Token").FirstOrDefault();
                if (string.IsNullOrEmpty(token))
                {
                    return Task<HttpResponseMessage>.Factory.StartNew(() =>
                    {
                        return new HttpResponseMessage(HttpStatusCode.BadRequest)
                        {
                            Content = new StringContent("Missing Authorization-Token")
                        };
                    });
                }
    
                var decryptedToken = RSAClass.Decrypt(token);
    
                // TODO: do your query to find the user
                var user = decryptedToken;
    
                var identity = new GenericIdentity(decryptedToken);
                string[] roles = new[] { "Users", "Testers" };
    
                var principal = new GenericPrincipal(identity, roles);
                Thread.CurrentPrincipal = principal;
            }
            catch
            {
                return Task<HttpResponseMessage>.Factory.StartNew(() =>
                {
                    return new HttpResponseMessage(HttpStatusCode.InternalServerError)
                    {
                        Content = new StringContent("Error encountered while attempting to process authorization token")
                    };
                });
            }
    
            return base.SendAsync(request, cancellationToken);
        }
    }
    
  6. Define a test controller:

    public class TestsController : Controller
    {
        public ActionResult GetProducts()
        {
            var productsUrl = Url.RouteUrl("DefaultApi", new { httproute = "", controller = "products" }, "http");
            using (var client = new HttpClient())
            {
                var token = RSAClass.Encrypt("john");
                client.DefaultRequestHeaders.Add("Authorization-Token", token);
    
                var products = client
                    .GetAsync(productsUrl)
                    .Result
                    .Content
                    .ReadAsAsync<IEnumerable<Product>>()
                    .Result;
    
                return Json(products, JsonRequestBehavior.AllowGet);
            }
        }
    
        public ActionResult PostProduct()
        {
            var productsUrl = Url.RouteUrl("DefaultApi", new { httproute = "", controller = "products" }, "http");
            using (var client = new HttpClient())
            {
                var token = RSAClass.Encrypt("john");
                client.DefaultRequestHeaders.Add("Authorization-Token", token);
    
                var product = new Product 
                {
                    Id = 1,
                    Name = "test product"
                };
    
                var result = client
                    .PostAsync<Product>(productsUrl, product, new JsonMediaTypeFormatter())
                    .Result;
                if (result.StatusCode == HttpStatusCode.Unauthorized)
                {
                    return Content("Sorry you are not authorized to perform this operation");
                }
    
                return Json(true, JsonRequestBehavior.AllowGet);
            }
        }
    }
    
  7. Test:

    * /tests/getproducts => success
    * /tests/postproduct => 401
    
Community
  • 1
  • 1
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • I have already registered the TokenValidation in global.asax.cs,but my question how do I make an authenticated token not access some other actions in the same controller...? I mean how do I accomplish [Authorize(Roles="Admin")] with the generated token in the "api controller" – CrazyNooB Aug 16 '12 at 12:35
  • You will have to write a different `TokenValidationHandler`. The one presented in this article stores only the username inside the token, no roles at all. You may checkout the following answer in which I illustrated a custom delegating handler using basic authentication scheme and relying on the built-in membership and roles providers: http://stackoverflow.com/a/11536349/29407 – Darin Dimitrov Aug 16 '12 at 12:37
  • Exactly that's what i was asking. Can you give a slight piece of code how to do role based authorization with the token..? – CrazyNooB Aug 16 '12 at 12:40
  • so I cannot use this "token approach" for authorization...? Does it work only for authentication...? – CrazyNooB Aug 16 '12 at 12:45
  • Simply adapt the code I wrote to work with a token instead of username/password. Currently my code simply reads the Authorization header coming from Basic Authorization. You could adapt the code to read your custom token header, decrypt it, obtain the username, and then use this username to query the role provider and obtain the roles for the user. In this case you no longer need to query the membership provider to verify the password because there's no longer a password => you suppose that if the client possess a valid token, it is a valid client. You only need to query the role provider. – Darin Dimitrov Aug 16 '12 at 13:00
  • Thanks for your help.It was really useful.I will try to do what you are saying. – CrazyNooB Aug 16 '12 at 13:17
  • just a quick question, how do I get the roles mentioned on an action inside delegation handler – CrazyNooB Aug 16 '12 at 13:36
  • You query the role provider: `string[] roles = Roles.Provider.GetRolesForUser(username);` and then you store them into the `GenericPrincipal`. – Darin Dimitrov Aug 16 '12 at 13:52
  • Roles.Provider.GetRolesForUser(username) will get the roles of an user, but on top of an action I will mention like this [Authorize(Roles="Users")], how do I get this "Users" role mentioned on top of an action inside delegating handler..? – CrazyNooB Aug 16 '12 at 14:21
  • Why do you need to get that inside your delegating handler? The delegating handler stores all the roles for the given user inside the GenericPrincipal and it is the Authorize attribute which checks whether this principal is in role. – Darin Dimitrov Aug 16 '12 at 14:33
  • So I just get the roles of user and store them into Generic Principal , the Authorize attribute will check whether the user is in the mentioned Roles...? – CrazyNooB Aug 16 '12 at 14:38
  • 404 error means not found. You are probably not using the correct url to call this action. – Darin Dimitrov Aug 17 '12 at 12:05
  • uh i know that :D , but why!! even when the user is in the mentioned roles it is showing the 404 error :| – CrazyNooB Aug 17 '12 at 13:55
  • url is correct if I remove [Authorize] from the api controller, then i am able to find products,but if i kept it as it is ,then i get 404 error – CrazyNooB Aug 17 '12 at 14:21
  • Who is throwing this 404 error? Without showing your code it is difficult to help you. – Darin Dimitrov Aug 17 '12 at 14:24
  • You've put `Roles="Administrator"` but I can't see you adding this role in the GenericPrincipal. – Darin Dimitrov Aug 17 '12 at 15:18
  • I have changed it to "Users" now, that does not help me, the same 404 comes – CrazyNooB Aug 17 '12 at 15:27
  • Alright, apparently I will have to spell it for you. See my updated answer explaining how to do this step by step. – Darin Dimitrov Aug 17 '12 at 17:02
  • Request.CreateResponse(HttpStatusCode.OK, products); does not work as it accepts only status code as parameter. And I am getting one more error with var products = client .GetAsync(productsUrl) .Result .Content .ReadAsAsync>() .Result; Error is No 'MediaTypeFormatter' is available to read an object of type 'IEnumerable`1' with the media type 'text/html'. – CrazyNooB Aug 20 '12 at 05:38
  • Which version of the Web API are you using? This should work with the RTM. – Darin Dimitrov Aug 20 '12 at 05:49
  • Thanks for your quick reply.I am a complete noob, where can I see the version of web api ? One thing I can say, I have downloaded this MVC 4 version 3 months ago. – CrazyNooB Aug 20 '12 at 06:28
  • So, it seems old. ASP.NET MVC 4 is RTM now. Download the latest version: http://www.microsoft.com/en-us/download/details.aspx?id=30683 – Darin Dimitrov Aug 20 '12 at 06:33
  • this is the runtime version of MVC 4 i have v4.0.30319. Anyways I am downloading the latest version now – CrazyNooB Aug 20 '12 at 06:36
  • this is working!!!!! but one more issue , when user does not belong to a role assigned on action, var products = client .GetAsync(productsUrl) .Result .Content .ReadAsAsync>() .Result; simply throws exception. I need to handle this exception. – CrazyNooB Aug 20 '12 at 09:47
  • Well handle it: didn't you see how I did this in the `PostProduct` action? You could test `if (result.StatusCode != HttpStatusCode.Unauthorized)` and only then attempt to read the Content. – Darin Dimitrov Aug 20 '12 at 09:56
  • guess i was not clear, the application execution itself stops at var products = client .GetAsync(productsUrl) .Result .Content .ReadAsAsync>() .Result; with an exception. I will try to fix it now. Ok I am checking it – CrazyNooB Aug 20 '12 at 10:00
  • Guess I was not clear too: read my answer and especially the `PostProduct` which handles this case. Do the same in your `GetProducts` action. Don't call `.Content.ReadAsAsync>().Result` initially. First check the status code and only if it is successful you can call this method on the result. – Darin Dimitrov Aug 20 '12 at 10:01
  • Thanks Darin for your amazing help, without your help this could not be done at all. Ignore my last edit :) – CrazyNooB Aug 20 '12 at 11:09
  • Made an update..guess this is what you meant in your last comment, am i correct..? – CrazyNooB Aug 20 '12 at 11:24
  • This has been a real help to me in getting my authorization at least debuggable, but it doesn't seem to work. I've adapted the handler in the above code to query my member and role data directly from EF and pass the roles into the Principle, but my Authorize attributes are NOT being respected at all when I specify a role - they just seem to pass regardless. What am I missing here? – Keith Jackson Sep 12 '13 at 10:18
  • To answer my own comment - This was a bit of a wild goose chase for me. My problem was the AllowAnonymous attribute seems to be overriding the lower level settings for Authorization – Keith Jackson Sep 12 '13 at 10:43