Web API Resource Authorizations - c#

I'm currently building a Web API but have run into an authorization problem with GET requests. Essentially my problem is how do I authorize requests based on the request parameters. I've tried looking online but the only solution I can find for resource authorization is to create my own authorization filter attribute. So lets say a client belongs to a Fac 8474, so they should be able to get api/fac/8474. But what is stopping the user from getting api/fac/8475? The code below uses a custom auth filter that gets the users claim value for nId. After getting the requested record I check the nId value on the record. If they are equal I allow access, if not I send a 401 unauthorized. However, I will have to write this code for every route and that I don't like the thought of writing extra code for each request which might include even more logic then a simple int check.
class ClaimsAuthorizationFilter : AuthorizationFilterAttribute
{
public override Task OnAuthorizationAsync(HttpActionContext actionContext, System.Threading.CancellationToken cancellationToken)
{
var principal = actionContext.RequestContext.Principal as ClaimsPrincipal;
KeyValuePair<string, object> facId = actionContext.RequestContext.RouteData.Values.Where(p => p.Key == "facId").SingleOrDefault();
Claim nIdClaim = principal.Claims.Where(p => p.Type == "nId").SingleOrDefault();
if (nIdClaim == null) {
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
return Task.FromResult<object>(null);
}
SM msg = ClientAPI.GenerateResponse("FAC_GET", Convert.ToInt32(facId.Value));
dtoFac fac = (dtoFac)msg.Data;
if (nIdClaim.Value != fac.nId.ToString())
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
return Task.FromResult<object>(null);
}
return Task.FromResult<object>(null);
}
}
I've also looked at this example http://jakeydocs.readthedocs.io/en/latest/security/authorization/resourcebased.html Essentially getting the resource then checking the nId which I also implemented and it works.
var identity = User.Identity as ClaimsIdentity;
Claim nId = identity.Claims.Where(p => p.Type == "nId").SingleOrDefault();
if (nId == null) {
return NotFound();
}
// Get list of resources named bat
if (!IEnumAuthCheck(bat, nId.Value)) {
return NotFound();
}
private bool IEnumAuthCheck(IEnumerable<bat> list, string nId)
{
foreach (bat element in list)
{
if (element.nId != Convert.ToInt32(nId))
{
return false;
}
}
return true;
}
My main question is, what if any are the recommended patterns for solving this issue? Resource based authentication for ids seem like it would be a very common issue but there is a distant lack of articles about it.

Related

.net Core WebAPI Forbid method returns status code 500 instead of 403 [duplicate]

I tried to solve this for hours now and I can not find anything. Basicly I have a simple controller which roughly looks like this:
[Route("v1/lists")]
public class ListController : Controller
{
...
[HttpPost("{id}/invite")]
public async Task<IActionResult> PostInvite([FromBody] string inviteSecret, [FromRoute] int id, [FromQuery] string userSecret)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
List list = await context.Lists.SingleOrDefaultAsync(l => l.ID == id);
if (list == null)
{
return NotFound();
}
User postingUser = await context.Users.SingleOrDefaultAsync(u => u.ID == list.CreationUserID);
if (postingUser == null || postingUser.Secret != userSecret)
{
return Forbid();
}
await context.ListInvites.AddAsync(new ListInvite{ListID = id, InviteSecret = inviteSecret});
await context.SaveChangesAsync();
return Ok();
}
....
}
The thing is: Whenever this method gets called and it exits through return Forbid();, Kestrel throws an InvalidOperationException afterwards with the message
No authentication handler is configured to handle the scheme: Automatic
(and of course the server returns a 500). What's strange about it is the fact that I am not doing any authentication whatsoever anywhere, and it does not happen e.g. if the method leaves with return Ok();. I'm really lost at this point because if you try to google this problem you get solutions over solutions... for people who actually do auth and have a problem with it. I really hope someone over here knows how to resolve this and/or what I could do to find out why this happens.
Like SignIn, SignOut or Challenge, Forbid relies on the authentication stack to decide what's the right thing to do to return a "forbidden" response: some authentication handlers like the JWT bearer middleware return a 403 response while others - like the cookie middleware - prefer redirecting the user to an "access denied page".
If you don't have any authentication handler in your pipeline, you can't use this method. Instead, use return StatusCode(403).

AntiForgery cookie in MVC3

I wanted to implement this solution to handle antiforgery in ajax requests. I know there are other solutions but this is the one I like most.
The problem is I have to deal with System.Web.Webpages 1.0 so I cannot make use of AntiForgeryConfig.CookieName in my code.
public override void OnAuthorization(AuthorizationContext filterContext)
{
var request = filterContext.HttpContext.Request;
// Only validate POSTs
if (request.HttpMethod == WebRequestMethods.Http.Post)
{
// Ajax POSTs and normal form posts have to be treated differently when it comes
// to validating the AntiForgeryToken
if (request.IsAjaxRequest())
{
string cookieName = AntiForgeryData.GetAntiForgeryTokenName(context.Request.ApplicationPath);
var antiForgeryCookie = request.Cookies[AntiForgeryConfig.CookieName];
var cookieValue = antiForgeryCookie != null
? antiForgeryCookie.Value
: null;
AntiForgery.Validate(cookieValue, request.Headers["__RequestVerificationToken"]);
}
else
{
new ValidateAntiForgeryTokenAttribute()
.OnAuthorization(filterContext);
}
}
}
How can I retrieve (programmatically) the cookie name set by the antiforgery system in Mvc3? I suspect the AntiForgery.Validate part will also be a problem but I'll handle that before. Any thoughts?
The actual cookie name always starts from "__RequestVerificationToken" with some suffix. So you can find the cookie like this:
private static string FindCookieValueByName(HttpRequestBase request)
{
return request.Cookies
.Cast<string>()
.Where(cn => cn.StartsWith("__RequestVerificationToken", StringComparison.OrdinalIgnoreCase))
.Select(cn => request.Cookies[cn].Value)
.FirstOrDefault();
}

Does an OpenID realm have to be the base URL of the web site?

As a continuation of this question, there's an issue I'm having with dotnetopenauth.
Basically, I'm wondering if the realm specified in the RP has to be the actual base URL of the application? That is, (http://localhost:1903)? Given the existing architecture in place it is difficult to remove the redirect - I tried setting the realm to the base OpenId controller (http://localhost:1903/OpenId) and testing manually did generate the XRDS document. However, the application seems to freeze, and the EP log reveals the following error:
2012-10-10 15:17:46,000 (GMT-4) [24] ERROR DotNetOpenAuth.OpenId - Attribute Exchange extension did not provide any aliases in the if_available or required lists.
Code:
Relying Party:
public ActionResult Authenticate(string RuserName = "")
{
UriBuilder returnToBuilder = new UriBuilder(Request.Url);
returnToBuilder.Path = "/OpenId/Authenticate";
returnToBuilder.Query = null;
returnToBuilder.Fragment = null;
Uri returnTo = returnToBuilder.Uri;
returnToBuilder.Path = "/";
Realm realm = returnToBuilder.Uri;
var response = openid.GetResponse();
if (response == null) {
if (Request.QueryString["ReturnUrl"] != null && User.Identity.IsAuthenticated) {
} else {
string strIdentifier = "http://localhost:3314/User/Identity/" + RuserName;
var request = openid.CreateRequest(
strIdentifier,
realm,
returnTo);
var fetchRequest = new FetchRequest();
request.AddExtension(fetchRequest);
request.RedirectToProvider();
}
} else {
switch (response.Status) {
case AuthenticationStatus.Canceled:
break;
case AuthenticationStatus.Failed:
break;
case AuthenticationStatus.Authenticated:
//log the user in
break;
}
}
return new EmptyResult();
}
Provider:
public ActionResult Index()
{
IRequest request = OpenIdProvider.GetRequest();
if (request != null) {
if (request.IsResponseReady) {
return OpenIdProvider.PrepareResponse(request).AsActionResult();
}
ProviderEndpoint.PendingRequest = (IHostProcessedRequest)request;
return this.ProcessAuthRequest();
} else {
//user stumbled on openid endpoint - 404 maybe?
return new EmptyResult();
}
}
public ActionResult ProcessAuthRequest()
{
if (ProviderEndpoint.PendingRequest == null) {
//there is no pending request
return new EmptyResult();
}
ActionResult response;
if (this.AutoRespondIfPossible(out response)) {
return response;
}
if (ProviderEndpoint.PendingRequest.Immediate) {
return this.SendAssertion();
}
return new EmptyResult();
}
The answer to your question is "no". The realm can be any URL between the base URL of your site and your return_to URL. So for example, if your return_to URL is http://localhost:1903/OpenId/Authenticate, the following are all valid realms:
http://localhost:1903/OpenId/Authenticate
http://localhost:1903/OpenId/
http://localhost:1903/
The following are not valid realms, given the return_to above:
http://localhost:1903/OpenId/Authenticate/ (extra trailing slash)
http://localhost:1903/openid/ (case sensitive!)
https://localhost:1903/ (scheme change)
Because some OpenID Providers such as Google issue pairwise unique identifiers for their users based on the exact realm URL, it's advisable for your realm to be the base URL to your web site so that it's most stable (redesigning your site won't change it). It's also strongly recommended that if it can be HTTPS that you make it HTTPS as that allows your return_to to be HTTPS and is slightly more secure that way (it mitigates DNS poisoning attacks).
The reason for the error in the log is because your RP creates and adds a FetchRequest extension to the OpenID authentication request, but you haven't initialized the FetchRequest with any actual attributes that you're requesting.
I couldn't tell you why your app freezes though, with the information you've provided.

What does IsReturnUrlDiscoverable do?

I'm using the following sample code from the DotnetOpenAuth Samples (OpenId Controller in OpenIdProviderMvc)
public ActionResult ProcessAuthRequest() {
if (ProviderEndpoint.PendingRequest == null) {
return this.RedirectToAction("Index", "Home");
}
// Try responding immediately if possible.
ActionResult response;
if (this.AutoRespondIfPossible(out response)) {
return response;
}
// We can't respond immediately with a positive result. But if we still have to respond immediately...
if (ProviderEndpoint.PendingRequest.Immediate) {
// We can't stop to prompt the user -- we must just return a negative response.
return this.SendAssertion();
}
return this.RedirectToAction("AskUser");
}
private bool AutoRespondIfPossible(out ActionResult response)
{
if (ProviderEndpoint.PendingRequest.IsReturnUrlDiscoverable(OpenIdProvider.Channel.WebRequestHandler) == RelyingPartyDiscoveryResult.Success
&& User.Identity.IsAuthenticated) {
if (ProviderEndpoint.PendingAuthenticationRequest != null) {
if (ProviderEndpoint.PendingAuthenticationRequest.IsDirectedIdentity
|| this.UserControlsIdentifier(ProviderEndpoint.PendingAuthenticationRequest)) {
ProviderEndpoint.PendingAuthenticationRequest.IsAuthenticated = true;
response = this.SendAssertion();
return true;
}
}
if (ProviderEndpoint.PendingAnonymousRequest != null) {
ProviderEndpoint.PendingAnonymousRequest.IsApproved = true;
response = this.SendAssertion();
return true;
}
}
response = null;
return false;
}
However, I don't want to ask the user anything. I'm trying to set up a web application portal that should automatically respond positively to the RP if the user is logged in (which he is). Yet AutoRespondIfPossible returns false, because ProviderEndpoint.PendingRequest.IsReturnUrlDiscoverable returns false and I'm not sure why. What action should I be taking here?
Logs:
RP: http://pastebin.com/0EX2ZE1C
EP: http://pastebin.com/q5CPrWp6
Previous related questions:
SSO - No OpenID endpoint found
OpenIdProvider.GetRequest() returns null
Does an OpenID realm have to be the base URL of the web site?
IsReturnUrlDiscoverable performs what OpenID calls "RP Discovery". And it's important anyway, but particularly if you will be auto-logging users in, it's critical for security. The fact that it's returning false tells you the RP needs some work to do this correctly.
This blog post explains what the RP must do to pass "RP Discovery" tests.

How can I return a BadRequest on Azure Mobile Services TableController GET template method?

I am using Azure Mobile Services (following the standard Azure TodoItems tutorial), and the most basic GET method that they provide is:
public IQueryable<MyModel> GetAllMyInfo()
{
return Query();
}
This works, but I am trying to extend it so that the method will only return MyModel data for an authenticated user (identified by the X-ZUMO-AUTH authentication header standard for Mobile Service API calls). So I modified the code for:
public IQueryable<MyModel> GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
var ownerId = currentUser.Id;
return Query().Where(s => s.OwnerId == ownerId);
}
This also works when a valid auth token is passed. However, if an invalid auth header is passed, then the currentUser is null, and the query fails (obviously). So I am trying to check for null and return a BadRequest or a 403 HTTP code. Yet a simple `return BadRequest("Invalid authentication") gives a compilation error:
public IQueryable<MyModel> GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
if(currentUser == null) {
return BadRequest("Database has already been created."); // This line gives a compilation error saying I need a cast.
}
var ownerId = currentUser.Id;
return Query().Where(s => s.OwnerId == ownerId);
}
Does anyone know how to check for a valid authentication token and return a 403 on this method (which wants an IQueryable return type?
You can use the [AuthorizeLevel] attribute on this method to indicate that a valid token must be present in order for the method to be invoked. It will return a 401 if not.
So your full method would be:
[AuthorizeLevel(AuthorizationLevel.User)]
public IQueryable<MyModel> GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
var ownerId = currentUser.Id;
return Query().Where(s => s.OwnerId == ownerId);
}
Please note that for the Azure Mobile Apps SDK (not Mobile Services), the above attribute is simply replaced with [Authorize].
I know this is a bit late, but will document here for you and others that may come looking for a similar problem.
(While agreeing with Matt that a 403 could/should be achieved with a [Authorize] attribute, the question is regarding returning a different HttpStatusCode OR IQueryable)
I had a similar scenario where I needed to validate some query parameters and either return my results or a HttpError (in my case I wanted a 404 with content).
I found 2 ways, either keeping the return as IQueryable<T> and throwing a HttpResponseException or changing the return to IHttpActionResult and returning normal with HttpStatusCode or Ok(Data).
I found to prefer the later as throwing an Exception would be breaking the execution while in debug and not a very pleasant development experience.
Option 1 (Preferred)
//Adding Return annotation for API Documentation generation
[ResponseType(typeof(IQueryable<MyModel>))]
public IHttpActionResult GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
if(currentUser == null) {
return BadRequest("Database has already been created.");
}
var ownerId = currentUser.Id;
return Ok(Query().Where(s => s.OwnerId == ownerId));
}
Option 2 (Throwing Exception)
public IQueryable<MyModel> GetAllMyInfo()
{
// Get the current user
var currentUser = User as ServiceUser;
if(currentUser == null) {
throw new HttpResponseException(System.Net.HttpStatusCode.BadRequest)
// Or to add a content message:
throw new HttpResponseException(new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) {
Content = new System.Net.Http.StringContent("Database has already been created.")
});
}
var ownerId = currentUser.Id;
return Query().Where(s => s.OwnerId == ownerId);
}

Categories