For my website i want following behaviors for secured controller(or action)
if a user makes a normal request redirect to login page (which i have easily able to do)
if request is Ajax type Request.IsAjaxRequest()==true, return status code 401
How can i create a filter for this??
public class MyCustomAuthorize : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
//if ajax request set status code and end Response
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.HttpContext.Response.End();
}
base.HandleUnauthorizedRequest(filterContext);
}
}
Create a filter like above, it will return status code 401 for unauthorized request if request is made thru ajax.
If you are using jQuery you can do as below
jQuery.ajax({
statusCode: {
401: function() {
alert('unauthrized');
},
/*other options*/
});
In addition to the accepted answer, I needed to put this line of code in to prevent FormsAuthentication from redirecting to the login page..
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
I then removed
filterContext.HttpContext.Response.End();
var unauthorizedResult = new JsonResult
{
Data = new ErrorResult() {Success = 0, Error = "Forbidden"},
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
// status code
filterContext.HttpContext.Response.StatusCode = (int) HttpStatusCode.Unauthorized;
// return data
filterContext.Result = unauthorizedResult;
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
}
Your problem is not with AJAX request, your problem is returning HTTP 401 Unauthorized response, because you use forms authentication. This response code tells the framework that it should redirect the user-agent to your login page with a HTTP 302 response instead. That's why it was easy to setup the "normal" request redirect - it's done automatically.
To answer your question, I had similar problem and the solution I ended up with was not using forms authentication. I implemented a custom authorization attribute that handles both cases manually instead. I'm not sure if this is the best approach, but it does work. I'm interested in what others think of this solution or what other solutions there are.
Fortunately, you can still use the FormsAuthentication class to handle cookies for you, but you have to delete the forms authentication configuration from your Web.config file. When the user logs in you use FormsAuthentication.SetAuthCookie to, well, set a cookie (you are probably doing this already). Second, in your authorization attribute, you get the cookie from the request and use FormsAuthentication.Decrypt to decrypt it. If it exists and is valid, you set the user in the HttpContext based on this cookie, because forms authentication won't do it for you anymore. If it doesn't you either redirect to the login page or return 401, depending on whether it's an AJAX call or not.
You can use ajaxonly to restrain access to ajax actionresult
You can just return a HttpUnauthorizedResult.
Note: This could cause the MVC framework to return you to the login page.
public ActionResult FailResult()
{
return new HttpUnauthorizedResult();
}
a simple way is to do a check in the SignIn action
public ActionResult SignIn()
{
if (Request.IsAjaxRequest())
{
// you could return a partial view that has this script instead
return Content("<script>window.location = '" + Url.Action("SignIn", "Account") + "'</script>");
}
...
return View();
Related
I have an ASP.NET MVC application with ActionFilters for Authentication and no Forms Authentication. "SegurancaAction" is the attribute responsible for validating authentication and exists in every controller endpoint except in the login ones (as expected).
I'm facing a problem in which sometimes I try to access one of my controllers and the GET request goes to my login endpoint. In the method Application_BeginRequest at Global.asax, I can see the very first attempt is at 'security/login' (the route to my login endpoint) instead of the one I want. I can also see this endpoint being called in debugging apps such as Fiddler, or ASP.NET Trace or Glimpse MVC5.
Besides calling the wrong action, once I login again this issue keeps happening for the same endpoint I was trying to access, redirecting my site to the login page over and over.
SegurancaAction:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Autenticacoes autenticacao = _authApp.IsAutenticado(filterContext.HttpContext.Session.SessionID);
if (autenticacao == null)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
filterContext.Result = new HttpStatusCodeResult(System.Net.HttpStatusCode.Unauthorized);
else
{
filterContext.HttpContext.Response.RedirectPermanent("/security/login");
return;
}
}
else
{
// other stuff
}
}
SecurityController:
[HttpPost]
[ConfigAction]
public ActionResult Login(vm_Login login)
{
if (ModelState.IsValid)
{
if (!String.IsNullOrEmpty(login.Login) && !String.IsNullOrEmpty(login.Senha))
{
Entidades entidade = _entidadeApp.GetByUsuarioSenha(login.Login, login.Senha);
if (entidade == null)
{
ViewBag.FalhaAutenticacao = "As credenciais informadas não conferem!";
return View("Login");
}
else
{
string encryptionKey = System.Configuration.ConfigurationManager.AppSettings["EncryptionKey"];
var a = _autenticacaoApp.Autenticar(entidade.Id, encryptionKey, login.Senha, HttpContext.Session.SessionID);
}
Response.RedirectPermanent("~/principal/index");
}
}
else
{
ViewBag.FalhaAutenticacao = "É necessário informar o usuario e a senha!";
}
return View();
}
All _autenticacaoApp.Autenticar(...) method does is to create an authentication entry on the database, it's a completely custom code.
Does anyone know why this issue happens? Sometimes I can reproduce it by deleting the cookies that contain ASP.NET_Session ID and RequestVerificationToken. So far I know those cookies are automatically generated and I notice that sometimes when I login again they are not re-generated.
I figured out the issue. It was this "RedirectPermanent" method being used here:
filterContext.HttpContext.Response.RedirectPermanent("/security/login");
It tells the browser that the resource I'm trying to access is no longer available and is now located at this new Url. The browser records this information and always redirects to the new resource.
I just changed it to use "Redirect" instead.
On our MVC website, we secure our endpoints with the Authorize attribute, however I would like to redirect the user to the login screen when this occurs. My first solution was to redirect on Application_EndRequest in Global.asax.cs:
protected void Application_EndRequest(object sender, EventArgs e)
{
if (Response.StatusCode == (int) HttpStatusCode.Unauthorized)
{
Response.ClearContent();
Response.Redirect("/Account/Login");
}
}
The problem with this is that some of our views are loaded by AJAX, in which case we do not want to show the login screen within dynamically-loaded div (this happens for example when the back button is pressed after logging out). It would be better if the request still returned the login screen, but with a 401 Unauthorized status code so that we can detect this event. Obviously this will not work with this approach because the redirection relies on a 302 status code.
Instead, I would rather 'insert' the login screen content inside Application_EndRequest with a 401 status code. So, instead of the Response.Redirect, I tried this:
...
Response.ClearContent();
Server.TransferRequest("~/Account/Login");
Response.StatusCode = (int) HttpStatusCode.Unauthorized;
...
The redirect works nicely, but because this performs a new request, I can't set the status code in this way, and I get a 200. Still no good for detecting an unauthorized request.
I also tried:
...
Response.ClearContent();
Server.Transfer("~/Account/Login");
Response.StatusCode = (int) HttpStatusCode.Unauthorized;
...
But I get an exception "Error executing child request for /Account/Login."
I also tried:
...
Response.ClearContent();
HttpContext.Current.RewritePath("~/Account/Login");
Response.StatusCode = (int) HttpStatusCode.Unauthorized;
Response.End();
...
But this throws an exception "Thread was being aborted". I'm not sure if the Response.End() is necessary but without it, I just get a standard IIS 401 error.
How can I return the content of my login screen but with a 401 status code?
Sending a 401 AND a ViewObject or RedirectObject is not how the MVC Framework was designed to work: -- a few posts have cleverly shown that returning a 401 with a View witn an authorization filter..
I specify the MVC Framework, but this is not even the prefered way to use the HTTP Protocol.
When you redirect in your MVC application, you have created a RedirectResult
When your MVC application returns with a call to the Controller's View() method, a ViewResult is created
Whether a Redirect or a View eventually reaches the client browser either,
an HTTP Return Code of 200 -- Success will be returned
Or
an HTTP Return Code of 302 -- Redirect will be returned
The reason for this convention is that, the server is "handling" the redirect. If the user is not authorized, the server will redirect the user to the login page.
Simlarly, in AJAX or asynchronous HTTP requests, so long as an HTML View is returned, the HTTP request is considered a success. Redirect (302) is not returned for asynchronous requests.
Sending Error Codes to client:
If you want to return a 401 unauthorized to the client, that usually means that you want to the client to "handle" the unauthorized request by navigating to the Login or Error View.
$.ajax({
url: loginURL,
data: $("#form").serialize(),
type: "POST",
dataType: "html"
})
.success(function (result, status) {
/*additional logic*/
})
.error(function (xhr, status) {
if (xhr.status == 401)
{
window.location = unauthorizedUrl; //redirect to unauthorized url
}
});
What if you want to Redirect to Login Page AND Display an Error
your question makes most sense if you want to redirect to a Login Page AND display an Error
Then, the convention is to Add your error message into Viewbag or a ModelState Error
such as:
var user = await signInManager.UserManager.FindByNameAsync(model.UserName);
ModelState.AddModelError("", "Invalid username or password.");
return View(model);
In your View, to have code that looks for an error:
A ValidationSummary would pickup the Model error
#Html.ValidationSummary(true)
This specific example of Bad Login Attempt doesn't exactly speak to your example.
I couldn't find an image for a user redirected because they were not authorized, but, in this case, the logic is similar. Error Msg can be populated with a QueryString, Viewbag, etc.
You would ideally want to use a custom AuthorizeAtrribute and then hook into the HandleUnauthorizedRequest method and from that, return a view to your login page by setting the controller and view to call in the AuthorizationContext.
public class MyAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.RouteData.Values["controller"] = "home";
filterContext.RouteData.Values["action"] = "login";
filterContext.Result = new ViewResult ();
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
}
}
}
note: The trick for this is to make sure you set the following value to true to avoid forms authentication taking over the response once you've set the 401 status.
HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
I tried quite a few different methods all of which had their own sneaky problems. Not sure how your solution is setup but if you need to perform a transfer you can use the following:
Custom AuthorizeAttribute
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
filterContext.RequestContext.HttpContext.Server.TransferRequest("/Account/Login/?NOT_AUTHORISED=TRUE", false);
}
}
You need to pass a message to the next request, using the QueryString seemed to do the trick. Although you need to remember that anyone can set a QS parameter.
Set Status Code
protected void Application_EndRequest(object sender, EventArgs e)
{
if (Request["NOT_AUTHORISED"] == "TRUE")
{
Response.StatusCode = (int) HttpStatusCode.Unauthorized;
}
}
Demo
There's no redirect and the HTTP Status code is set to 401.
We have an internal ASP.NET MVC application that requires a logon. Log on works great and does what's expected. We have a session expiration of 15 minutes. After sitting on a single page for that period of time, the user has lost the session. If they attempt to refresh the current page or browse to another, they will get a log on page. We keep their request stored so once they've logged in they can continue on to the page that they've requested. This works great.
However, my issue is that on some pages there are AJAX calls. For example, they may fill out part of a form, wander off and let their session expire. When they come back, the screen is still displayed. If they simply fill in a box (which will make an AJAX call) the AJAX call will return the Logon page (inside of whatever div the AJAX should have simply returned the actual results). This looks horrible.
I think that the solution is to make the page itself expire (so that when a session is terminated, they automatically are returned to the logon screen without any action by them). However, I'm wondering if there are opinions/ideas on how best to implement this specifically in regards to best practices in ASP.NET MVC.
Update:
So I went ahead and implemented this in my OnActionExecuting (per Keltex's suggestion)
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.Write("Invalid session -- please login!");
filterContext.HttpContext.Response.End();
}
else
{
...
}
}
This definitely makes things better -- now even if they have two tabs (one with some AJAX calls that they can trigger) and they log out explicitly in the second tab, they will immediately get something that makes more sense rather than a bunch of screwed up AJAX data.
I still think I will implement the Javascript countdown as well that womp suggested.
Specifically, I don't know that there are any best practices regarding it, but I'm doing this right now for our app. We've opted for a client-side solution where we output the Session timeout value into some javascript in the master page, and calculate when the session will expire.
5 minutes before-hand, we pop up a modal dialog box saying "Are you still there?" with a countdown timer. Once the timer hits 0:00, we redirect the browser to the login page.
It's implemented with a minimal amount of javascript to do the time and timer calculations, and a simple .ashx handler that will refresh the session if the user clicks "I'm back!" on the dialog box before the session expires. That way if they return in time, they can refresh the session without any navigation.
I asked similar question yesterday. Here is my solution:
Modified Authorize attribute:
public class OptionalAuthorizeAttribute : AuthorizeAttribute
{
private class Http403Result : ActionResult
{
public override void ExecuteResult(ControllerContext context)
{
// Set the response code to 403.
context.HttpContext.Response.StatusCode = 403;
context.HttpContext.Response.Write(CTRes.AuthorizationLostPleaseLogOutAndLogInAgainToContinue);
}
}
private readonly bool _authorize;
public OptionalAuthorizeAttribute()
{
_authorize = true;
}
//OptionalAuthorize is turned on on base controller class, so it has to be turned off on some controller.
//That is why parameter is introduced.
public OptionalAuthorizeAttribute(bool authorize)
{
_authorize = authorize;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
//When authorize parameter is set to false, not authorization should be performed.
if (!_authorize)
return true;
var result = base.AuthorizeCore(httpContext);
return result;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
//Ajax request doesn't return to login page, it just returns 403 error.
filterContext.Result = new Http403Result();
}
else
base.HandleUnauthorizedRequest(filterContext);
}
}
HandleUnauthorizedRequest is overridden, so it returns Http403Result when using Ajax. Http403Result changes StatusCode to 403 and returns message to the user in response. There is some additional logic in attribute (authorize parameter), because I turn on [Authorize] in base controller and disable it in some pages.
Other important part is global handling of this response on client side. This is what I placed in Site.Master:
<script type="text/javascript">
$(document).ready(
function() {
$("body").ajaxError(
function(e,request) {
if (request.status == 403) {
alert(request.responseText);
window.location = '/Logout';
}
}
);
}
);
</script>
I place GLOBAL ajax error handler and when evert $.post fails with 403 error, response message is alerted and user is redirected to logout page. Now I don't have to handle error in every $.post request, because it is handled globally.
Why 403, not 401? 401 is handled internally by MVC framework (that is why redirection to login page is done after failed authorization).
What do you think about it?
EDIT:
About resigning from [Authorize] attribute: [Authorize] is not only about checking Identity.IsAuthenticated. It also handles page caching (so you don't cache material that requires authentication) and redirection. There is no need to copy this code.
You might look into the AjaxOptions that can be set in Ajax.BeginForm(). There is an OnBegin setting that you can associate with a javascript function, which could call a Controller method to confirm that the session is still valid, and if not, redirect to the login page using window.location.
Part of the problem appears to be that you're letting the framework do everything. I wouldn't decorate your AJAX method with the [Authorize] attribute. Instead check User.Identity.IsAuthenticated and if it returns false, create sensible error message.
My solution uses one meta-tag on login form and a bit of Javascript/jQuery.
LogOn.cshtml
<html>
<head>
<meta data-name="__loginform__" content="true" />
...
</head>
...
</html>
Common.js
var Common = {
IsLoginForm: function (data) {
var res = false;
if (data.indexOf("__loginform__") > 0) {
// Do a meta-test for login form
var temp =
$("<div>")
.html(data)
.find("meta[data-name='__loginform__']")
.attr("content");
res = !!temp;
}
return res;
}
};
AJAX code
$.get(myUrl, myData, function (serverData) {
if (Common.IsLoginForm(serverData)) {
location.reload();
return;
}
// Proceed with filling your placeholder or whatever you do with serverData response
// ...
});
Here's how I did it...
In my base controller
protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.HttpContext.Response.StatusCode = 403;
filterContext.HttpContext.Response.Write(SessionTimeout);
filterContext.HttpContext.Response.End();
}
}
}
Then in my global .js file
$.ajaxSetup({
error: function (x, status, error) {
if (x.status == 403) {
alert("Sorry, your session has expired. Please login again to continue");
window.location.href = "/Account/Login";
}
else {
alert("An error occurred: " + status + "nError: " + error);
}
}
});
The SessionTimeout variable is a noty string. I omitted the implementation for brevity.
I have a method decorated with two custom ActionFilterAttribute.
[RequiresAuthentication(Order = 1)]
[ToonAction(Order = 2)]
public ActionResult Browse(...
RequiresAuthentication attribute is coming from this article
Inside RequiresAuthentication, on it's OnActionExecuting I do:
filterContext.HttpContext.Response.Redirect(loginUrl, true);
The line is get executed, and the arguments are all as expected. The problem is that after executing the line above, I get next attribute (ActionFilterAttribute) executed, as if redirect didn't work, it just continues executing the request, instead of simply redirecting browser.
Question: what else do I need to do to make the request handler
This is a complete method:
public override void OnActionExecuting(ActionExecutingContext filterContext) {
//redirect if not authenticated
var identity = filterContext.HttpContext.User.Identity;
if (!identity.IsAuthenticated) {
//use the current url for the redirect
string redirectOnSuccess = filterContext.HttpContext.Request.Url.PathAndQuery;
//send them off to the login page
string redirectUrl = string.Format("?ReturnUrl={0}", redirectOnSuccess);
string loginUrl = FormsAuthentication.LoginUrl + redirectUrl;
filterContext.HttpContext.Response.Redirect(loginUrl, true);
// filterContext.Result = new HttpUnauthorizedResult();
// filterContext.HttpContext.Response.StatusCode = 0x191;
}
}
You want to set the Result on the filterContext to a RedirectResult, not do a redirect on the response.
filterContext.Result = new RedirectResult { Url = loginUrl };
EDIT: As #Hunter Daley suggests a better mechanism would be to use the AuthorizeAttribute instead if it works for you. If you do have authentication/authorization scenarios that the AuthorizeAttribute doesn't work for, it would probably be better to derive your custom attribute from it instead of the more generic ActionFilterAttribute. In any event, the correct technique is to set the Result rather than interact with the Response directly. You might want to look at the actual AuthorizeAttribute source at http://www.codeplex.com/aspnet for ideas.
I've got a sample of custom authorization code on my blog, http://farm-fresh-code.blogspot.com, too.
try adding the [Authorize] attribute to your Action methods instead
Add
filterContext.HttpContext.Response.Clear();
at first
and this at End :
filterContext.HttpContext.Response.End();
Hope this helps.
you can use
return RedirectToAction("Index", "Home/Login", new {area = "", returnURL = Request.Url.AbsolutePath});
to stop the current processing, redirect to the desired (login) page and quit the action.
the area route property is needed to get out of the current area if you are in any.
Add this code before you redirect the page.
filterContext.ExceptionHandled = true;
I have some action methods behind an Authorize like:
[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Create(int siteId, Comment comment) {
The problem I have is that I'm sending a request through AJAX to Comment/Create with
X-Requested-With=XMLHttpRequest
which helps identify the request as AJAX. When the user is not logged in and hits the Authorize wall it gets redirected to
/Account/LogOn?ReturnUrl=Comment%2fCreate
which breaks the AJAX workflow. I need to be redirected to
/Account/LogOn?X-Requested-With=XMLHttpRequest
Any ideas how that can be achieved? Any ways to gain more control over what happens when Authorization is requested?
Thanks to Lewis comments I was able to reach this solution (which is far from perfect, posted with my own comments, if you have the fixes feel free to edit and remove this phrase), but it works:
public class AjaxAuthorizeAttribute : AuthorizeAttribute {
override public void OnAuthorization(AuthorizationContext filterContext) {
base.OnAuthorization(filterContext);
// Only do something if we are about to give a HttpUnauthorizedResult and we are in AJAX mode.
if (filterContext.Result is HttpUnauthorizedResult && filterContext.HttpContext.Request.IsAjaxRequest()) {
// TODO: fix the URL building:
// 1- Use some class to build URLs just in case LoginUrl actually has some query already.
// 2- When leaving Result as a HttpUnauthorizedResult, ASP.Net actually does some nice automatic stuff, like adding a ReturnURL, when hardcodding the URL here, that is lost.
String url = System.Web.Security.FormsAuthentication.LoginUrl + "?X-Requested-With=XMLHttpRequest";
filterContext.Result = new RedirectResult(url);
}
}
}
Recently I ran into exactly the same problem and used the code posted by J. Pablo Fernández
with a modification to account for return URLs. Here it is:
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
override public void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
// Only do something if we are about to give a HttpUnauthorizedResult and we are in AJAX mode.
if (filterContext.Result is HttpUnauthorizedResult && filterContext.HttpContext.Request.IsAjaxRequest())
{
// TODO: fix the URL building:
// 1- Use some class to build URLs just in case LoginUrl actually has some query already.
HttpRequestBase request = filterContext.HttpContext.Request;
string returnUrl = request.Path;
bool queryStringPresent = request.QueryString.Count > 0;
if (queryStringPresent || request.Form.Count > 0)
returnUrl += '?' + request.QueryString.ToString();
if (queryStringPresent)
returnUrl += '&';
returnUrl += request.Form;
String url = System.Web.Security.FormsAuthentication.LoginUrl +
"?X-Requested-With=XMLHttpRequest&ReturnUrl=" +
HttpUtility.UrlEncode(returnUrl);
filterContext.Result = new RedirectResult(url);
}
}
}
Instead of using the authorize attribute, I've been doing something like the following.
public ActionResult SomeCall(string someData)
{
if (Request.IsAjaxRequest() == false)
{
// TODO: do the intended thing.
}
else
{
// This should only work with AJAX requests, so redirect
// the user to an appropriate location.
return RedirectToAction("Action", "Controller", new { id = ?? });
}
}
I think the right way to handle this would be in your Javascript making the AJAX call.
If the user needs to be authorized (or authenticated as your code implies) and isn't, you should inform them and maybe not allow them to try and comment in the first place.
However, if that doesn't suit your needs.
You could try and write your own authorize action filter, maybe inheriting from the one that comes with the MVC framework but redirects how you want it to. It's fairly straightforward.