How to do role based authorization for asp.net mvc 4 web api
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.
- Create a new ASP.NET MVC 4 project using the Empty Template
-
Define a model:
public class Product { public int Id { get; set; } public string Name { get; set; } }
-
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) { } }
-
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") { } } }
-
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); } }
-
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); } } }
-
Test:
* /tests/getproducts => success * /tests/postproduct => 401
CrazyNooB
I am the greatest noob ever lived :D(my english is not correct too)
Updated on October 25, 2020Comments
-
CrazyNooB over 3 years
I am trying to make a secure asp.net web api. For that I have followed the below link
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 over 11 yearsI 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 over 11 yearsYou 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 over 11 yearsExactly that's what i was asking. Can you give a slight piece of code how to do role based authorization with the token..?
-
CrazyNooB over 11 yearsso I cannot use this "token approach" for authorization...? Does it work only for authentication...?
-
Darin Dimitrov over 11 yearsSimply 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 over 11 yearsThanks for your help.It was really useful.I will try to do what you are saying.
-
CrazyNooB over 11 yearsjust a quick question, how do I get the roles mentioned on an action inside delegation handler
-
Darin Dimitrov over 11 yearsYou query the role provider:
string[] roles = Roles.Provider.GetRolesForUser(username);
and then you store them into theGenericPrincipal
. -
CrazyNooB over 11 yearsRoles.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 over 11 yearsWhy 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 over 11 yearsSo 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 over 11 years404 error means not found. You are probably not using the correct url to call this action.
-
CrazyNooB over 11 yearsuh i know that :D , but why!! even when the user is in the mentioned roles it is showing the 404 error :|
-
CrazyNooB over 11 yearsurl 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 over 11 yearsWho is throwing this 404 error? Without showing your code it is difficult to help you.
-
Darin Dimitrov over 11 yearsYou've put
Roles="Administrator"
but I can't see you adding this role in the GenericPrincipal. -
CrazyNooB over 11 yearsI have changed it to "Users" now, that does not help me, the same 404 comes
-
Darin Dimitrov over 11 yearsAlright, apparently I will have to spell it for you. See my updated answer explaining how to do this step by step.
-
CrazyNooB over 11 yearsRequest.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 over 11 yearsWhich version of the Web API are you using? This should work with the RTM.
-
CrazyNooB over 11 yearsThanks 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 over 11 yearsSo, it seems old. ASP.NET MVC 4 is RTM now. Download the latest version: microsoft.com/en-us/download/details.aspx?id=30683
-
CrazyNooB over 11 yearsthis is the runtime version of MVC 4 i have v4.0.30319. Anyways I am downloading the latest version now
-
CrazyNooB over 11 yearsthis 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 over 11 yearsWell handle it: didn't you see how I did this in the
PostProduct
action? You could testif (result.StatusCode != HttpStatusCode.Unauthorized)
and only then attempt to read the Content. -
CrazyNooB over 11 yearsguess 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 over 11 yearsGuess I was not clear too: read my answer and especially the
PostProduct
which handles this case. Do the same in yourGetProducts
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 over 11 yearsThanks Darin for your amazing help, without your help this could not be done at all. Ignore my last edit :)
-
CrazyNooB over 11 yearsMade an update..guess this is what you meant in your last comment, am i correct..?
-
Keith Jackson over 10 yearsThis 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 over 10 yearsTo 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