.NET MVC Check header before allowing response - c#

I have created a Login system that provides the user with an API-key on successful login. The API key has an access level attached to it.
I am sending the API key in the header with every Ajax request from my front end, however it is no longer sending a correct response.
This is one of my controllers:
[AllowCrossSiteJson]
public class WebOrdersController : Controller
{
protected IWebOrdersService _webOrdersService = new WebOrdersService();
// GET: WebOrders
public ActionResult Index()
{
return View();
}
[WebOrdersAPIFilter]
public String GetTotals()
{
return _webOrdersService.GetAllTotals();
}
}
And this is my Filter class:
public class WebOrdersAPIFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
IAccountsService _accountsService = new AccountsService();
filterContext.RequestContext.HttpContext.Response.AddHeader("Authority", "*");
var rawUrl = HttpContext.Current.Request.RawUrl;
var routeArray = rawUrl.Split('/');
var route = routeArray[0].ToLower();
if (String.IsNullOrEmpty(route))
{
route = routeArray[1].ToLower();
}
var headers = filterContext.HttpContext.Request.Headers;
// Ensure that all of your properties are present in the current Request
if (!String.IsNullOrEmpty(headers["Authority"]))
{
var apiKey = headers["Authority"];
//GET API LEVEL
int accessLevel = _accountsService.CheckAPIKey(apiKey);
if(route == "weborders" && (accessLevel != 1 || accessLevel != 3 || accessLevel != 6))
{
filterContext.Result = new RedirectResult("http://localhost/error/unauthorised");
}
}
else
{
filterContext.Result = new RedirectResult("http://localhost/error/notloggedin");
}
base.OnActionExecuting(filterContext);
}
}
If the user is not logged in, the API Key is not sent in the header, so if it does not exist it is safe to say they are logged out. If they are logged in, The key is sent. I am then getting the access level of they key and comparing it to the route.
I have debugged it and found that the route and accessLevel variables are working correctly however even then it does not return and response and if the IF() statement fails, it is returning the html for the page /error/unauthorised or /error/notloggedin and not actually redirecting.
How can I achieve the functionality i require, to ensure that the user requesting the data is authorised?

Related

C# Insert Query Parameter in MVC Insufficient Role Redirection

I have recently implemented Roles in MVC application. The controller has various action methods like below:
public class MyController : Controller
[Authorize(Roles = "User, SuperUser")]
public ActionResult DoActionOne() {
// This can be invoked by both user and superuser
}
[Authorize(Roles = "SuperUser")]
public ActionResult DoActionTwo() {
// This can't be invoked by user
// Application will redirect to /Login automatically when 'User' invokes it
}
}
Now, this works good, as desired.
But, I want that when User is redirected to /Login due to insufficient Role to access Action method DoActionTwo in controller, I want to insert a query param, which can be read by /Login page to show message to user like "Invalid Permissions / Authorization Required". It may be any custom message.
Any ideas ?
#SimpleGuy - Check the below code with your "OnAuthorizationAsync" method.
You can manage all the flag with unnecessary request
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// Allow Anonymous skips all authorization
if (context.Filters.Any(item => item is IAllowAnonymousFilter))
{
return;
}
var policyEvaluator = context.HttpContext.RequestServices.GetRequiredService<IPolicyEvaluator>();
var authenticateResult = await policyEvaluator.AuthenticateAsync(Policy, context.HttpContext);
var authorizeResult = await policyEvaluator.AuthorizeAsync(Policy, authenticateResult, context.HttpContext, context);
if (authorizeResult.Challenged)
{
// Return custom 401 result
context.Result = new CustomUnauthorizedResult("Authorization failed.");
}
else if (authorizeResult.Forbidden)
{
// Return default 403 result
context.Result = new ForbidResult(Policy.AuthenticationSchemes.ToArray());
}
}
This is your main code to change
if (authorizeResult.Challenged)
{
// Return custom 401 result
context.Result = new CustomUnauthorizedResult("Authorization failed.");
}
else if (authorizeResult.Forbidden)
{
// Return default 403 result
context.Result = new ForbidResult(Policy.AuthenticationSchemes.ToArray());
}

ActionFilters for getting the user ID and returning error if it is null

I have a base class from which my (api) controllers inherit.
I made it so that i can get the user id from my authentication provider and use it later to do stuff with the user, update it or get its data, etc.
public class BaseController : ControllerBase
{
protected readonly IBaseData _baseData;
public BaseController(IBaseData baseData)
{
_baseData = baseData;
}
public Guid GetUserId()
{
string nameIdentifier = User.FindFirst(ClaimTypes.NameIdentifier).Value;
Guid? userId = _baseData.GetInvestorId(nameIdentifier);
return userId != null ? (Guid)userId : Guid.Empty;
}
}
I then call it inside my API end points:
Guid userId = GetUserId();
BaseModel m = _userData.GetBaseModel(userId);
return Ok(m);
Pretty simple. It gets called in multiple places in the controller.
Not ideal but works fine.
However now i need to catch an error that sometimes happens where the user is not in the DB.
I can add some code to the API end point to do that like this:
Guid userId = GetUserId();
if (userId == Guid.Empty)
return NotFound(new ResponseGapiModel { Response = Response.NotFound, Message = "user not found in DB" });
BaseModel m = _userData.GetBaseModel(userId);
return Ok(m);
But that wold mean i would repeat a lot of code all over the place.
I have been trying to use an action filter instead. But cannot get my head around it.
I don't know how to pass parameters inside the actionfilter, like the name identifier i need to find the user. nor frankly how pass the ID back.
-UPDATE-
I have now managed to get the actionfilter to return a failed result when the user is not found so half of what i need works. Problem is that now i call the DB twise as i still call the original BaseCass GetUserId to get the ID to be used in later methods.
To get the ActionFilter to check for the missing user i injected my datacontext into it:
private readonly NodeDBContext _context;
public ValidateAuth0UserInDBAttribute(NodeDBContext context)
{
_context = context;
}
as well as used the HttpContext from the ActionExecutingContext to find my user Nameidentifier:
public void OnActionExecuting(ActionExecutingContext context)
{
//check if the user it in DB
var id = _context.Users.SingleOrDefault(i => i.NameIdentifier == context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value)?.Id;
if (id == null)
{
context.Result = new NotFoundObjectResult(new ResponseModel { Response = Response.UserNotFound, Message = "User not found in DB" });
return;
}
}
The problem is now how do i get the "id" passed back from this to my controller? Is there a way? or do i have to call the DB twice?
The problem is now how do i get the "id" passed back from this to my controller?
Try to use below code to pass id back to controller:
public class MyActionFilter : Attribute,IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
var id = _context.Users.SingleOrDefault(i => i.NameIdentifier == context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value)?.Id;
if (id == null)
{
context.Result = new NotFoundObjectResult(new ResponseModel { Response = Response.UserNotFound, Message = "User not found in DB" });
return;
}
var controller = (ControllerBase)context.Controller;
controller.HttpContext.Items.Add("CurrentUserId", id );
}
public void OnActionExecuted(ActionExecutedContext context) { }
}
Action:
[MyActionFilter]
public IActionResult Get()
{
var id = HttpContext.Items["CurrentUserId"]?.ToString();
//...
}

Getting values from Custom AuthorizeAttribute being default

I have the follow Custom AuthorizeAttribute:
public class SystemAuthorizeAttribute : AuthorizeAttribute
{
public Form PermissionForm { get; set; } //Enum
public PermissionValue Permissions { get; set; }//Enum
public override void OnAuthorization(AuthorizationContext filterContext)
{
//Request is an Authenticated Method?
if (filterContext.HttpContext.Request.IsAuthenticated)
{
Debug.WriteLine("Test 1 " + PermissionForm);
if (!CurrentUserHasPermissionForm(PermissionForm))
{
//Deny access code
}
}
}
//...
}
After Login method it redirects to Index page from HomeController. The problem is when use SystemAuthorize Attribute in my HomeController the Form value always come as 0 when it should be 4 (Content).
HomeController method:
[SystemAuthorize(PermissionForm = Form.CONTENT, Permissions = PermissionValue.VIEW)]
public ActionResult Index()
{
return this.View();
}
Login method:
[AllowAnonymous]
[Route("Account/Login")]
public ActionResult Login(LoginViewModel model, string url = "")
{
var user= GetUserAccount(model);
if (user == null)
{
ModelState.AddModelError("", "User not found!");
return View(model);
}
else
{
FormsAuthentication.SetAuthCookie(user.Sign, false);
var authTicket = new FormsAuthenticationTicket(1, user.Sign, DateTime.Now, DateTime.Now.AddMinutes(20), false, JsonConvert.SerializeObject(user));
var authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(authTicket));
HttpContext.Response.Cookies.Add(authCookie);
return RedirectToAction("Index", "Home");
}
}
Form enum:
public enum Form : short
{
PATIENT = 1,
USERS = 2,
MEDICE = 3,
CONTENT = 4,
}
What I'm doing wrong or missing?
Unfortunately Microsoft made this a bit confusing by combining IAuthorizationFilter with Attribute in the same class. The fact of the matter is that attributes cannot do anything except store meta-data.
The part of MVC that reads the attribute is the IAuthorizationFilter which through some MVC magic is registered with MVC automatically when you place AuthorizeAttribute (or a subclass) on a controller or action.
But the only way to actually read the meta-data from the attribute is to use Reflection. The meta-data is in the same class, but not the same instance of the class. The meta-data is in the Attribute, but the code that executes when the filter runs is in the IAuthorizationFilter, which is a separate instance of the same class.
public class SystemAuthorizeAttribute : AuthorizeAttribute
{
public Form PermissionForm { get; set; } //Enum
public PermissionValue Permissions { get; set; }//Enum
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
var actionDescriptor = httpContext.Items["ActionDescriptor"] as ActionDescriptor;
if (actionDescriptor != null)
{
var authorizeAttribute = this.GetSystemAuthorizeAttribute(actionDescriptor);
// If the authorization attribute exists
if (authorizeAttribute != null)
{
// Run the authorization based on the attribute
var form = authorizeAttribute.PermissionForm;
var permissions = authorizeAttribute.Permissions;
// Return true if access is allowed, false if not...
if (!CurrentUserHasPermissionForm(form))
{
//Deny access code
}
}
}
return true;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
// Pass the current action descriptor to the AuthorizeCore
// method on the same thread by using HttpContext.Items
filterContext.HttpContext.Items["ActionDescriptor"] = filterContext.ActionDescriptor;
base.OnAuthorization(filterContext);
}
private SystemAuthorizeAttribute GetSystemAuthorizeAttribute(ActionDescriptor actionDescriptor)
{
SystemAuthorizeAttribute result = null;
// Check if the attribute exists on the action method
result = (SystemAuthorizeAttribute)actionDescriptor
.GetCustomAttributes(attributeType: typeof(SystemAuthorizeAttribute), inherit: true)
.SingleOrDefault();
if (result != null)
{
return result;
}
// Check if the attribute exists on the controller
result = (SystemAuthorizeAttribute)actionDescriptor
.ControllerDescriptor
.GetCustomAttributes(attributeType: typeof(SystemAuthorizeAttribute), inherit: true)
.SingleOrDefault();
return result;
}
}
Note that OnAuthorization has some logic in it that you will need to support output caching and the part of the code that checks for [AllowAnonymous], so you should not put your authorization check there, but in AuthorizeCore. But unfortunately, AuthorizeCore isn't passed the ActionDescriptor you need to check whether the attribute exists, so you need the above httpContext.Items hack to ensure it is passed into that method on the same thread.
The Reflection part becomes much more clear if you separate your Attribute into a different class from the IAuthorizationFilter, as in this example.

Custom ASP.NET MVC Forms Authentication

I want to do custom authentication because we have many controllers and it makes sense to create global filter that applies for all controllers and their actions with exception of login page.
In Global.asax.cs I added next global filter:
public class Global : HttpApplication
{
void Application_Start(object sender, EventArgs e) // Code that runs on application startup
{
... // only showing important part
GlobalFilters.Filters.Add(new Filters.AuthenticationUserActionFilter());
...
}
File AuthenticationUserActionFilter.cs:
public class AuthorizeUserActionFilter : System.Web.Mvc.Filters.IAuthenticationFilter
{
public void OnAuthentication(AuthenticationContext filterContext)
{
bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousActionFilter), inherit: true) || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousActionFilter), inherit: true);
if (skipAuthorization) // anonymous filter attribute in front of controller or controller method
return;
// does this always read value from ASPXAUTH cookie ?
bool userAuthenticated = filterContext.HttpContext.User.Identity.IsAuthenticated;
if (!userAuthenticated)
{
filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary() { { "controller", "Account" }, { "action", "Login" } });
return;
}
if( HttpContext.Current.User as Contracts.IUser == null )
{
// check if IUser is stored in session otherwise retrieve from db
// System.Web.HttpContext.Current.User is reseted on every request.
// Is it ok to set it from Session on every request? Is there any other better approach?
if (HttpContext.Current.Session["User"] != null && HttpContext.Current.Session["User"] as Contracts.IUser != null)
{
HttpContext.Current.User = HttpContext.Current.Session["User"] as Contracts.IUser;
}
else
{
var service = new LoginService();
Contracts.ISer user = service.GetUser(filterContext.HttpContext.User.Identity.Name);
HttpContext.Current.Session["User"] = user;
HttpContext.Current.User = user;
}
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext) {}
}
My login code is like this (in AccountController.cs):
[Filters.AllowAnonymousActionFilter]
[HttpPost]
public JsonResult Login(string username, string password, bool rememberMe = false)
{
LoginService service = new LoginService();
Contracts.IUser user = service .Login(username, password);
System.Web.HttpContext.Current.Session["User"] = value;
System.Web.HttpContext.Current.User = value;
// set cookie i.e. ASPX_AUTH, if remember me, make cookie persistent, even if user closed browser
if (System.Web.Security.FormsAuthentication.IsEnabled)
System.Web.Security.FormsAuthentication.SetAuthCookie(username, rememberMe);
return new SuccessResponseMessage().AsJsonNetResult();
}
Contracts.IUser interface:
public interface IUser : IPrincipal
{
Contracts.IUserInfo UserInfo { get; }
Contracts.ICultureInfo UserCulture { get; }
}
My question is this:
System.Web.HttpContext.Current.User is reseted on every request. Is it ok to set HttpContext.Current.User with Session value on every request? Is there any other better approach? What is best practise? Also Microsoft seems to have multiple ways of dealing with this problem (googled a lot of articles on this, also on stackoverflow Custom Authorization in Asp.net WebApi - what a mess?). There is a lot of confusion about this, although they developed a new Authorization in asp.net core.
One possible approach is to serialize the user as part of the UserData portion of the ASPXAUTH cookie. This way you don't need to fetch it from the database on each request and you don't need to use Sessions (because if you use sessions in a web-farm you will have to persist this session somewhere like in a database, so you will be round-tripping to the db anyway):
[Filters.AllowAnonymousActionFilter]
[HttpPost]
public JsonResult Login(string username, string password, bool rememberMe = false)
{
LoginService service = new LoginService();
Contracts.IUser user = service.Login(username, password);
string userData = Serialize(user); // Up to you to write this Serialize method
var ticket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddHours(24), rememberMe, userData);
string encryptedTicket = FormsAuthentication.Encrypt(ticket);
Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket));
return new SuccessResponseMessage().AsJsonNetResult();
}
And then in your custom authorization filter you could decrypt the ticket and authenticate the user:
public void OnAuthentication(AuthenticationContext filterContext)
{
... your stuff about the AllowAnonymousActionFilter comes here
var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie == null)
{
// Unauthorized
filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary() { { "controller", "Account" }, { "action", "Login" } });
return;
}
// Get the forms authentication ticket.
var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
Contracts.ISer user = Deserialize(authTicket.UserData); // Up to you to write this Deserialize method -> it should be the reverse of what you did in your Login action
filterContext.HttpContext.User = user;
}

How to bypass System.Web.Http.AuthorizeAttribute.IsAuthorized

In our server, CORS is already enabled, so that scripts like ajax may communicate in our API. But this is only effective on API that has no SecureAttribute
This one is working properly:
[CorsPreflightEnabled]
public class DevicesController : ApiController
{
[CorsEnabled]
[HttpPost]
public bool Register(DTO::ClientInfo info)
While this one is always rejected:
[CorsPreflightEnabled]
[Http::Secure]
public class UserController : ApiController
{
[CorsEnabled]
[HttpPost]
public bool AddClaims(Domain::DTO.UserClaim claim)
This is the code for SecureAttribute:
public class SecureAttribute : AuthorizeAttribute
{
protected override bool IsAuthorized(System.Web.Http.Controllers.HttpActionContext actionContext)
{
var calltoken = HttpContext.Current.Request.Headers["Device"] ?? "";
var token = JsonConvert.DeserializeObject<DeviceCallToken>(calltoken) ?? new DeviceCallToken();
var cachetoken = new ClientAuthentication().VerifyDevice(token);
if (cachetoken != null)
{
// if a cachetoken was successfully extracted from our records,
// then store the information into the principal for possible reuse
var principal = AppPrincipal.Current;
var identity = principal.Identity as AppIdentity;
identity.ServiceHeader.SessionId = token.SessionId;
identity.ServiceHeader.ClientKey = cachetoken.ClientKey;
identity.ServiceHeader.DeviceCode = cachetoken.DeviceCode;
identity.ServiceHeader.Merchant = cachetoken.Merchant;
Thread.CurrentPrincipal = principal;
HttpContext.Current.User = principal;
}
return cachetoken != null && !string.IsNullOrWhiteSpace(cachetoken.Salt);
}
}
When I call the API using ajax the Method is set to OPTIONS and the value of calltoken is always null.
Now my question is, how can I bypass checking the security when the method is OPTIONS?
I found that, if I try to put values in calltoken via breakpoint, the IsAuthorized will be called again for the last time, and from there the Device header has now value.
I really hope that I explained myself well. If not, I may have to show some images.
EDIT: Working code
public class SecureAttribute : AuthorizeAttribute
{
protected override bool IsAuthorized(System.Web.Http.Controllers.HttpActionContext actionContext)
{
var calltoken = HttpContext.Current.Request.Headers["Device"] ?? "";
var token = JsonConvert.DeserializeObject<DeviceCallToken>(calltoken) ?? new DeviceCallToken();
var cachetoken = new ClientAuthentication().VerifyDevice(token);
if (cachetoken != null)
{
// if a cachetoken was successfully extracted from our records,
// then store the information into the principal for possible reuse
var principal = AppPrincipal.Current;
var identity = principal.Identity as AppIdentity;
identity.ServiceHeader.SessionId = token.SessionId;
identity.ServiceHeader.ClientKey = cachetoken.ClientKey;
identity.ServiceHeader.DeviceCode = cachetoken.DeviceCode;
identity.ServiceHeader.Merchant = cachetoken.Merchant;
Thread.CurrentPrincipal = principal;
HttpContext.Current.User = principal;
}
else
{
var originalRequest = actionContext.Request;
var isCorsRequest = originalRequest.Headers.Contains("Origin");
if (originalRequest.Method == HttpMethod.Options && isCorsRequest)
{
// Allow to penetrate
return true;
}
}
return cachetoken != null && !string.IsNullOrWhiteSpace(cachetoken.Salt);
}
}
There is no way to bypass the authorisation attribute, that would be pretty insecure if it were possible. Your options are:
Remove the attribute from the methods you need to call.
Pass the correct security headers in the ajax call.
Modify the SecureAttribute class to allow OPTIONS method (but that sounds pretty insecure too)
Create new methods for ajax that don't have the attribute attached.
I'd suggest option 2.

Categories

Resources