We are developing an ASP.NET MVC web application using .NET Framework 4.5 and claims-based authentication/authorization. The application uses the standard ASP.NET mechanism to save a token into a cookie to read claims between POSTs that have been previously cached into memory.
We have configured the session timeout in the web.config file like this:
<sessionState timeout="1" mode="InProc"/>
When a session timeout occurs, the Session_End event handler is called:
protected void Session_End(object sender, EventArgs e)
{
Session.RemoveAll();
FederatedAuthentication.SessionAuthenticationModule.SignOut();
}
After Session_End is executed, the Session_Start handler is triggered:
protected void Session_Start(object sender, EventArgs e)
{
Breadcrumb breadcrumb = new Breadcrumb();
Session["Breadcrumb"] = breadcrumb;
}
This behavior causes a custom filter checking whether there is session or not to return always true:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class SessionExpireFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// Get context
HttpContext ctx = HttpContext.Current;
// If the browser session has expired...
if (ctx.Session != null && ctx.Session["Breadcrumb"] == null)
{
// Redirect to login page
}
}
}
On the other hand, if the user clicks the "Log out" menu option, the following handler is called:
public ActionResult Logout()
{
FederatedAuthentication.SessionAuthenticationModule.SignOut();
return RedirectToAction("Login", "Account");
}
After this, if we enter a valid application URL into the browser, the page is shown without being prompted for credentials. Does this mean that the token is still valid?
Could you please tell us what is happening?
Thanks very much in advance for your help.
Related
I am using ASPNet Identity 2.0 (Full framework, not the core framework) and MVC.
I would like to execute C# code once the user successfully login to the site.
i know that i can write some code right after the SignInManager.PasswordSignInAsync command and it will work for new login but not will not work for users who used "remember me" feature and returned to the site later (Cookie authentication).
I am looking for an option to catch the event of all the users who signed in to the site either by entering the password and by using the "remember me" cookie.
One way you can do it is by handling the application event in your global.asax file.
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
if (Request.IsAuthenticated)
{
Response.Write("HELLO");
}
}
There are many ways to do it. You can create and use custom attribute.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class CustomAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
readonly IAuthentication _authentication;
public CustomAuthorizeAttribute(IAuthentication authentication)
{
_authentication = authentication;
}
public override void OnAuthorization(AuthorizationContext filterContext)
{
if (!_authentication.Authorize(filterContext.HttpContext))
filterContext.Result = new HttpUnauthorizedResult();
//your code .....
}
}
And now rather than using [Authorize] use your new [CustomAuthorizeAttribute] attribute
[CustomAuthorizeAttribute]
public ActionResult Index()
{
ViewBag.Title = "Welcome";
ViewBag.Message = "Welcome to ASP.NET MVC!";
return View();
}
What you're after is FormsAuthentication_OnAuthenticate (I appreciate Forms-based authentication was not mentioned in the question, but it's an example of Cookie-based remember me authentication, adapt at will)
Unfortunately, there is no trigger for OnCookieBasedFormsAuthenticate_SessionCreate :)
So what you can do is check the Forms-based authentication every so often, because it (and Application_AuthenticateRequest) is fired for every request, CSS pages, images etc, going off to the database to check multiple times per request is an overly resource hungry idea. Luckily the forms cookie ticket has an issued on date and we can use that to check:
public void FormsAuthentication_OnAuthenticate(object sender, FormsAuthenticationEventArgs args)
{
if (FormsAuthentication.CookiesSupported)
{
if (Request.Cookies[FormsAuthentication.FormsCookieName] != null)
{
try
{
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(
Request.Cookies[FormsAuthentication.FormsCookieName].Value);
if ((DateTime.Now - ticket.IssueDate).TotalMinutes > 10)
{
if(/*Insert logic to check username here with ticket.Name*/)
{
//recreate cookie with new issuedate
FormsAuthentication.SetAuthCookie(ticket.Name, ticket.IsPersistent);
}
else
{
FormsAuthentication.SignOut();
}
}
Debug.WriteLine($"{ticket.Name} {ticket.IssueDate.ToUniversalTime()}");
}
catch (Exception e)
{
//Elmah
ErrorSignal.FromCurrentContext().Raise(e);
//Cannot decrypt cookie, make the user sign in again
FormsAuthentication.SignOut();
}
}
}
else
{
throw new HttpException("Cookieless Forms Authentication is not supported for this application.");
}
}
This is not the same as cookie expiration, because the user will still retain login status; the user should never see any login request unless there account is no longer valid.
My MVC application uses FormsAuthentication to sign in.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(FiNext.Models.User user)
{
try
{
using (HttpClient httpClient = new HttpClient())
{
var task = httpClient.PostAsJsonAsync<FiNext.Models.User>(String.Concat(ServiceUri.ApiUrl,"/Test/Validate"), user).Result;
FormsAuthentication.SetAuthCookie(user.Name, false);
SessionHelpers.UserId = user.Id;
return RedirectToAction("Create");
}
}
And it has a Session time out of 1 minute(in web.config) and once the session time out is called ,I am clearing sessions in session_end event in Global.asax.
protected void Session_End(object sender, EventArgs e)
{
Session.Clear();
Session.Abandon();
}
Now the problem when i sign out using normal log off button on the page,the page gets signed out.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
try
{
FormsAuthentication.SignOut();
return RedirectToAction("Home", "User");
}
catch (Exception ex)
{
throw ex;
}
}
and now i hit any url of this application(say "http://abcd.com/User/UserList") it is redirected to login page as we have logged out and redirecting to home page.
This is the desired functionality and working fine.
But the problem is when there is session time out and session_end event is fired.And now when i hit any url of this application(say "http://abcd.com/User/UserList"),iam able to get the data(which should not happen).
So how to signout from forms authentication when session_end is fired.
I tried this in session_end event in Global.asax:
protected void Session_End(object sender, EventArgs e)
{
FormsAuthentication.SignOut();
Session.Clear();
Session.Abandon();
}
but its gives "Object reference not set to an instance of an object." exception.
Maybe I am missing something, but it sounds like an authorization problem, not a session problem.
Is your UserList action secured in some way?
[Authorize]
public ActionResult UserList()
{
return View();
}
http://msdn.microsoft.com/en-us/library/ff398049(v=vs.100).aspx
I currently use the default forms authentication method for my ASP.NET MVC application.
above all of my actionmethods that require authentication I have this attribute
[Authorize()]
When someone tries to call the page that that action method "serves" and they haven't yet logged in, it sends them to the login page...perfect! However, if their session times out and they try to hit that page, they're also just redirected to the login page with no indication of why. I'd like to be able to determine if it's a new visit, or if it's a timeout and display a different message on the login screen accordingly.
Is that possible?
Have a look at this custom authorize attribute i have made. It was to implement some custom role based authorization, but you could make it work for you as well. There is a Session.IsNewSession property you can check to see if this request takes place on a new session.
public class CustomAuthorizeAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
if (httpContext.User.Identity.IsAuthenticated)
{
httpContext.User = new GenericPrincipal(httpContext.User.Identity, AdminUserViewModel.Current.SecurityGroups.Select(x => x.Name).ToArray());
}
return base.AuthorizeCore(httpContext);
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.User.Identity.IsAuthenticated)
{
filterContext.Result = new RedirectResult("/Authentication/NotAuthorized", false);
}
else
{
if (filterContext.HttpContext.Session.IsNewSession)
{
// Do Something For A New Session
}
base.HandleUnauthorizedRequest(filterContext);
}
}
}
On sign-in, you can set a cookie that's tied to the browser session. If that cookie exists, you know that the session timed out. If not, you know it's a new visit.
I need to redirect users to the Change Password page if their password has expired.
I want to place this code in one place so that any request can be redirected to the change password page.
I've looked into extending the AuthorizeAttribute, and overriding OnActionExecuting, but neither work/allow me to short circuit the routing logic to redirect to the password change page.
For a little clarification, the logic would be:
Unauthorized request:
-> any URL -> AuthorizeAttribute -> Login.aspx -> password expired -> ChangePassword.aspx
Authorized request:
-> any URL -> ??????? -> ChangePassword.aspx
Its that ???? part that I'm not sure what to do.
I think I'm going to go with extending the AuthorizeAttribute. I'll use that everywhere except the password change controller methods.
public class DenyExpiredPasswordAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
IPrincipal user = filterContext.HttpContext.User;
if(user != null)
{
if (user.Identity.IsAuthenticated)
{
if (CurrentUser.PasswordExpired) // your checking of password expiration
{
filterContext.HttpContext.Response.Redirect("~/Account/ChangePassword?reason=expired");
}
}
}
base.OnAuthorization(filterContext);
}
}
this works fine, just mark every controller with this attribute exclude "Account" one. This way no user with expired attribute able to continue until change password.
You could look at adding an event handler for the PostAuthenticateRequest event in global.asax.
protected void Application_Start(object sender, EventArgs e) {
this.PostAuthenticateRequest += new EventHandler(Global_PostAuthenticateRequest);
}
void Global_PostAuthenticateRequest(object sender, EventArgs e)
{
if (passwordExpired) {
Context.Response.Redirect("~/ChangePassword.aspx");
}
}
How to disable standard ASP.NET handling of 401 response code (redirecting to login page) for AJAX/JSON requests?
For web-pages it's okay, but for AJAX I need to get right 401 error code instead of good looking 302/200 for login page.
Update:
There are several solutions from Phil Haack, PM of ASP.NET MVC - http://haacked.com/archive/2011/10/04/prevent-forms-authentication-login-page-redirect-when-you-donrsquot-want.aspx
In classic ASP.NET you get a 401 http response code when calling a WebMethod with Ajax. I hope they'll change it in future versions of ASP.NET MVC. Right now I'm using this hack:
protected void Application_EndRequest()
{
if (Context.Response.StatusCode == 302 && Context.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
Context.Response.Clear();
Context.Response.StatusCode = 401;
}
}
The ASP.NET runtime is developed so that it always will redirect the user if the HttpResponse.StatusCode is set to 401, but only if the <authentication /> section of the Web.config is found.
Removing the authentication section will require you to implement the redirection to the login page in your attribute, but this shouldn't be a big deal.
I wanted both Forms authentication and to return a 401 for Ajax requests that were not authenticated.
In the end, I created a custom AuthorizeAttribute and decorated the controller methods. (This is on .Net 4.5)
//web.config
<authentication mode="Forms">
</authentication>
//controller
[Authorize(Roles = "Administrator,User"), Response302to401]
[AcceptVerbs("Get")]
public async Task<JsonResult> GetDocuments()
{
string requestUri = User.Identity.Name.ToLower() + "/document";
RequestKeyHttpClient<IEnumerable<DocumentModel>, string> client =
new RequestKeyHttpClient<IEnumerable<DocumentModel>, string>(requestUri);
var documents = await client.GetManyAsync<IEnumerable<DocumentModel>>();
return Json(documents, JsonRequestBehavior.AllowGet);
}
//authorizeAttribute
public class Response302to401 : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new JsonResult
{
Data = new { Message = "Your session has died a terrible and gruesome death" },
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
filterContext.HttpContext.Response.StatusCode = 401;
filterContext.HttpContext.Response.StatusDescription = "Humans and robots must authenticate";
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
}
}
//base.HandleUnauthorizedRequest(filterContext);
}
}
You could also use the Global.asax to interrupt this process with something like this:
protected void Application_PreSendRequestHeaders(object sender, EventArgs e) {
if (Response.StatusCode == 401) {
Response.Clear();
Response.Redirect(Response.ApplyAppPathModifier("~/Login.aspx"));
return;
}
}
I don't see what we have to modify the authentication mode or the authentication tag like the current answer says.
Following the idea of #TimothyLeeRussell (thanks by the way), I created a customized Authorize attribute (the problem with the one of #TimothyLeeRussell is that an exception is throw because he tries to change the filterContext.Result an that generates a HttpException, and removing that part, besides the filterContext.HttpContext.Response.StatusCode = 401, the response code was always 200 OK). So I finally resolved the problem by ending the response after the changes.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class BetterAuthorize : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
//Set the response status code to 500
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
filterContext.HttpContext.Response.StatusDescription = "Humans and robots must authenticate";
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
filterContext.HttpContext.Response.End();
}
else
base.HandleUnauthorizedRequest(filterContext);
}
}
You can call this method inside your action,
HttpContext.Response.End();
Example
public async Task<JsonResult> Return401()
{
HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
HttpContext.Response.End();
return Json("Unauthorized", JsonRequestBehavior.AllowGet);
}
From MSDN: The End method causes the Web server to stop processing the script and return the current result. The remaining contents of the file are not processed.
You could choose to create a custom FilterAttribute implementing the IAuthorizationFilter interface.
In this attribute you add logic to determine if the request are supposed to return JSON. If so, you can return an empty JSON result (or do whatever you like) given the user isn't signed in. For other responses you would just redirect the user as always.
Even better, you could just override the OnAuthorization of the AuthorizeAttribute class so you don't have to reinvent the wheel. Add the logic I mentioned above and intercept if the filterContext.Cancel is true (the filterContext.Result will be set to an instance of the HttpUnauthorizedResult class.
Read more about "Filters in ASP.NET MVC CodePlex Preview 4" on Phil Haacks blog. It also applies to the latest preview.