How to do role based authorization for asp.net mvc 4 web api

32,182

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
    
Share:
32,182
CrazyNooB
Author by

CrazyNooB

I am the greatest noob ever lived :D(my english is not correct too)

Updated on October 25, 2020

Comments

  • CrazyNooB
    CrazyNooB over 3 years

    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
    CrazyNooB over 11 years
    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"
  • Darin Dimitrov
    Darin Dimitrov over 11 years
    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: stackoverflow.com/a/11536349/29407
  • CrazyNooB
    CrazyNooB over 11 years
    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
    CrazyNooB over 11 years
    so I cannot use this "token approach" for authorization...? Does it work only for authentication...?
  • Darin Dimitrov
    Darin Dimitrov over 11 years
    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.
  • CrazyNooB
    CrazyNooB over 11 years
    Thanks for your help.It was really useful.I will try to do what you are saying.
  • CrazyNooB
    CrazyNooB over 11 years
    just a quick question, how do I get the roles mentioned on an action inside delegation handler
  • Darin Dimitrov
    Darin Dimitrov over 11 years
    You query the role provider: string[] roles = Roles.Provider.GetRolesForUser(username); and then you store them into the GenericPrincipal.
  • CrazyNooB
    CrazyNooB over 11 years
    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..?
  • Darin Dimitrov
    Darin Dimitrov over 11 years
    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.
  • CrazyNooB
    CrazyNooB over 11 years
    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...?
  • Darin Dimitrov
    Darin Dimitrov over 11 years
    404 error means not found. You are probably not using the correct url to call this action.
  • CrazyNooB
    CrazyNooB over 11 years
    uh i know that :D , but why!! even when the user is in the mentioned roles it is showing the 404 error :|
  • CrazyNooB
    CrazyNooB over 11 years
    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
  • Darin Dimitrov
    Darin Dimitrov over 11 years
    Who is throwing this 404 error? Without showing your code it is difficult to help you.
  • Darin Dimitrov
    Darin Dimitrov over 11 years
    You've put Roles="Administrator" but I can't see you adding this role in the GenericPrincipal.
  • CrazyNooB
    CrazyNooB over 11 years
    I have changed it to "Users" now, that does not help me, the same 404 comes
  • Darin Dimitrov
    Darin Dimitrov over 11 years
    Alright, apparently I will have to spell it for you. See my updated answer explaining how to do this step by step.
  • CrazyNooB
    CrazyNooB over 11 years
    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<IEnumerable<Products>>() .Result; Error is No 'MediaTypeFormatter' is available to read an object of type 'IEnumerable`1' with the media type 'text/html'.
  • Darin Dimitrov
    Darin Dimitrov over 11 years
    Which version of the Web API are you using? This should work with the RTM.
  • CrazyNooB
    CrazyNooB over 11 years
    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.
  • Darin Dimitrov
    Darin Dimitrov over 11 years
    So, it seems old. ASP.NET MVC 4 is RTM now. Download the latest version: microsoft.com/en-us/download/details.aspx?id=30683
  • CrazyNooB
    CrazyNooB over 11 years
    this is the runtime version of MVC 4 i have v4.0.30319. Anyways I am downloading the latest version now
  • CrazyNooB
    CrazyNooB over 11 years
    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<IEnumerable<Products>>() .Result; simply throws exception. I need to handle this exception.
  • Darin Dimitrov
    Darin Dimitrov over 11 years
    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.
  • CrazyNooB
    CrazyNooB over 11 years
    guess i was not clear, the application execution itself stops at var products = client .GetAsync(productsUrl) .Result .Content .ReadAsAsync<IEnumerable<Products>>() .Result; with an exception. I will try to fix it now. Ok I am checking it
  • Darin Dimitrov
    Darin Dimitrov over 11 years
    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<IEnumerable<Products>>().Result initially. First check the status code and only if it is successful you can call this method on the result.
  • CrazyNooB
    CrazyNooB over 11 years
    Thanks Darin for your amazing help, without your help this could not be done at all. Ignore my last edit :)
  • CrazyNooB
    CrazyNooB over 11 years
    Made an update..guess this is what you meant in your last comment, am i correct..?
  • Keith Jackson
    Keith Jackson over 10 years
    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
    Keith Jackson over 10 years
    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