6

I made a functionality that prevents multiple-login for one username at the same time and I call it in Actions like this:

        int userId = (int)WebSecurity.CurrentUserId;

        if ((this.Session.SessionID != dba.getSessionId(userId)) || dba.getSessionId(userId) == null)
        {
            WebSecurity.Logout();
            return RedirectToAction("Index", "Home");
        }

So the point is that every time user logins I save his sessionID into database field. So if someone with same username logins over someone already logged in with same username it overwrites that database field with this new session. If sessionID in DB is not the same as current session ID of logined user it log him out.

Is there a possibility to put this part of code in 1 place or do I have to put it in every single Action in my application?

I tried in Global.asax:

void Application_BeginRequest(object sender, EventArgs e)
    {
        if (Session["ID"] != null)
        {
            int userId = Convert.ToInt32(Session["ID"]);
            if ((this.Session.SessionID != db.getSessionId(userId)) || db.getSessionId(userId) == null)
            {
                WebSecurity.Logout();
            }
        }
    }

But I can't use Session here nor WebSecurity class if I try like this:

    void Application_BeginRequest(object sender, EventArgs e)
    {
        int userId = (int)WebSecurity.CurrentUserId;

        if ((this.Session.SessionID != db.getSessionId(userId)) || db.getSessionId(userId) == null)
        {
            WebSecurity.Logout();
            Response.RedirectToRoute("Default");                
        }
    }

because I get null reference exception.

EDIT

I used this:

    void IActionFilter.OnActionExecuting(ActionExecutingContext filterContext)
    {
        int userId = (int)WebSecurity.CurrentUserId;
        using (var db = new UsersContext())
        {
            string s = db.getSessionId(userId);

            if ((filterContext.HttpContext.Session.SessionID != db.getSessionId(userId)) || db.getSessionId(userId) == null)
            {
                WebSecurity.Logout();
                filterContext.Result = new RedirectResult("/Home/Index");
            }
        }
    }

I had to use using statement for context, otherwise db.getSessionId(userId) was returning old sessionId. Method is this:

    public string getSessionId(int userId)
    {
        string s = "";
        var get = this.UserProfiles.Single(x => x.UserId == userId);
        s = get.SessionId;
        return s;
    }

Very strange, will have to read about why that happened.

Everything works fine, except one thing. I have one JsonResult action in a controller, which returns Json, but since event(its textbox on enter event) can't trigger POST(I assume it's because it logs out before) redirect doesn't work. It can't even post to that Json action to receive callback and redirect. Any clues on that?

                        success: function (data) {
                        if (data.messageSaved) {
                            //data received - OK!
                        }
                        else {
                            // in case data was not received, something went wrong redirect out
                            window.location.href = urlhome;
                        }
                    }

Before I used ActionFilterAttribute I used code to check different sessions inside of POST and of course it could make callback and therefore redirect if didn't receive the data.. But now since it can't even POST and go into method it just stucks there and doesn't redirect :)

sensei
  • 7,044
  • 10
  • 57
  • 125

6 Answers6

4

I would derive from AuthorizeAttribute. No need to check this information if you don't need to authorize the request.

public class SingleLoginAuthorizeAttribute : AuthorizeAttribute
{
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    { 
        bool isAuthorized = base.AuthorizeCore(httpContext);

        if (isAuthorized)
        {
            int userId = (int)WebSecurity.CurrentUserId;

            if ((filterContext.HttpContext.Session.SessionID != dba.getSessionId(userId)) 
                || dba.getSessionId(userId) == null)
            {
                WebSecurity.Logout();
                isAuthorized = false;
                filterContext.Result = new RedirectResult("/Home/Index");
            }
        }

        return isAuthorized;
    }

    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAjaxRequest())
        {
            filterContext.Result = new JsonResult()
            {
                Data = FormsAuthentication.LoginUrl,
                JsonRequestBehavior = JsonRequestBehavior.AllowGet
            };
        }
        else
        {
            base.HandleUnauthorizedRequest(filterContext);
        }
    }
}

I'd also mention that this allows you to short circuit other ActionFilters because they run after OnAuthorization.

  1. Forward Order - OnAuthorization : AuthorizationFilter (Scope Controller)
  2. Forward Order - OnActionExecuting : ActionFilter1 (Scope Global)
  3. Forward Order - OnActionExecuting : ActionFilter2 (Scope Controller)
  4. Forward Order - OnActionExecuting : ActionFilter3 (Scope Action)

Then as Rob Lyndon mentioned, you could in the FilterConfig (MVC4)

public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new SingleLoginAuthorizeAttribute());
    }
}

Then when you don't want to require any authorization, you can use the AllowAnonymouseAttribute on your ActionResult methods or Controller Class to allow anonymous access.

Update

I added a way for your ajax calls (Get or Post) to work with timeouts. You can do something like:

success: function (jsonResult)
{
  if (jsonResult.indexOf('http') == 0)
  {
    window.location = jsonResult;
  }

  // do other stuff with the Ajax Result
}

This isn't exactly the best way, but if you want more information on how to do this better I would ask another question instead of appending more questions on this one.

Community
  • 1
  • 1
Erik Philips
  • 53,428
  • 11
  • 128
  • 150
  • +1 for the idea of deriving from the `AuthorizeAttribute`, but there is the small wrinkle that you're overriding its ability to skip authorization using the `AllowAnonymousAttribute`. – Rob Lyndon Aug 08 '13 at 23:13
  • Actually not, because two fold, first I'm checking to see if Request [IsAuthenticated](http://msdn.microsoft.com/en-us/library/system.web.httprequest.isauthenticated.aspx), and secondly it's running the `base.OnAuthorization()` which the source code looks exactly like `bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true) || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true);` (MVC 4 Source Code) – Erik Philips Aug 08 '13 at 23:29
  • Unless you're saying that the OP wants to allow only 1 anonymous user on the website at a time, which is not what the OP stated `So the point is that every time user logins I save his sessionID into database field.` – Erik Philips Aug 08 '13 at 23:31
  • 1
    I've just been looking at the source myself. What you can do is to override the `AuthorizeCore` method; then you don't need that `base.OnAuthorization()` call, and `AllowAnonymous` would let you opt out of the entire security check, which is what I would expect it to do. – Rob Lyndon Aug 08 '13 at 23:34
  • 1
    That's looking good. Then if you add that as a global filter, you can move from an opt-in security model to an opt-out security model. – Rob Lyndon Aug 08 '13 at 23:44
  • 1
    I always Opt-In to Opt-Out :) – Erik Philips Aug 08 '13 at 23:45
  • Wow this is high science for me out there hehe. I simplified the code, didn't use Authorization. I just want to point out that I tried you code, but the problem was near `base.OnAuthorization(filterContext);` says OnAuthorizaion doesn't exist. Maybe I forgot to add some reference, but it didn't give any choice. Anyway I used your redirection advice, but I have a problem with JsonResult action, since It can't callback it doesn't redirect :) Any suggestion with that? – sensei Aug 09 '13 at 01:03
  • Yes, you found a Typo, should have been `Base.AuthorizeCore()` which is fixed in the code above, and if you're doing ajax calls take a look at [Handling session timeout in ajax calls](http://stackoverflow.com/questions/5238854/handling-session-timeout-in-ajax-calls/5242746#5242746) – Erik Philips Aug 09 '13 at 01:09
  • You're not using the `AthorizeAttribute`? How is any of your code secure? – Erik Philips Aug 09 '13 at 01:10
  • No no, I use AuthorizeAttribute of course, but didn't use it in this context with custom ActionFilterAttribute. I am reading your comments chat between Rob and you, and will implement your code when I see what is going on about. Can you please check my Edit question if you have a time? thank you – sensei Aug 09 '13 at 01:14
  • Not sure if you missed it or mistyped it, but I would *NOT* avocate using an `ActionFilterAttribute`, my solution is deriving from the `AuthorizeAttribute` (which also does *not* derive from `ActionFilterAttribute`). – Erik Philips Aug 09 '13 at 01:18
  • Also I commented on your Ajax already with the [Handling session timeout in ajax calls](http://stackoverflow.com/questions/5238854/handling-session-timeout-in-ajax-calls/5242746#5242746). – Erik Philips Aug 09 '13 at 01:20
  • Oh very nice, I will check it! By the way `Base.AuthorizeCore()` is accepting different parameter type than filterContext object is. cannot convert from 'System.Web.Mvc.AuthorizationContext' to 'System.Web.HttpContextBase' – sensei Aug 09 '13 at 01:23
  • @JohnMathilda fixed that bug. Thanks. – Erik Philips Aug 09 '13 at 01:27
  • Thank you very much for everything you were very helpful! – sensei Aug 09 '13 at 01:29
  • 1
    As Erik says, this may not be the forum for it, but you can actually add a filter provider that can distinguish between Ajax and non-Ajax requests, and adjust the attribute accordingly. If you open up another question, we can take you through that process. The other advantage of doing that is that it will be easy to get a connection to your database using the filter provider -- and it will be a great deep dive into MVC. – Rob Lyndon Aug 09 '13 at 09:15
  • Thank you for your input Rob, I understand a lot more now. I don't know how to invite people to my question(can't find any message option here), so I am pasting the link to my new question here: http://stackoverflow.com/questions/18146382/how-to-use-custom-authorizeattribute-with-ajax Thanks a lot. – sensei Aug 09 '13 at 12:15
3

The ActionFilterAttribute is the way to go.

Pierre.Vriens
  • 2,117
  • 75
  • 29
  • 42
user2639740
  • 1,173
  • 10
  • 11
2

We created an Action Filter called SeatCheck and decorate each controller like this:

[SeatCheck]
public class NoteController : BaseController
{

We use that to get a count of seats and other functions, but it makes it so much easier to control everywhere without thinking about it.

In the proejct ActionFilters folder we have the SeatCheck.cs file that looks like this:

namespace site.ActionFilters
{
  public class SeatCheckAttribute : ActionFilterAttribute
  {
     public override void OnActionExecuting(ActionExecutingContext filterContext)
     {

You can get the SessionID in the Action Filter like this

 filterContext.HttpContext.Session.SessionID
davids
  • 5,397
  • 12
  • 57
  • 94
1

Yes, indeed there is. You can use an attribute derived from ActionFilterAttribute.

I would write a class called SessionSecurityAttribute:

public class SessionSecurityAttribute : ActionFilterAttribute
{
    public MyDbConn MyDbConn { get; set; }

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var session = filterContext.RequestContext.HttpContext.Session;
        if (session["ID"] != null && WebSecurity.IsAuthenticated)
        {
            int userId = Convert.ToInt32(session["ID"]);
            if ((sessionID != MyDbConn.getSessionId(userId)) || MyDbConn.getSessionId(userId) == null)
            {
                WebSecurity.Logout();
            }
        }
    }
}

The question remains: how can you add these attributes to your actions whilst giving them access to your database? That's easy: in Global.asax you can call into the bootstrapping RegisterGlobalFilters method:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
    filters.Add(new HandleErrorAttribute());
    filters.Add(new SessionSecurityAttribute
        {
            MyDbConn = DependencyResolver.Current.GetService<MyDbConn>()
        });
}

This adds your SessionSecurityAttribute, complete with DB connection, to every action by default, without a line of repeated code.

Rob Lyndon
  • 12,089
  • 5
  • 49
  • 74
  • 1
    Thank you for your help. I just want to add that `Session["ID"]` doesn't work here. You probably just forgot to add like `filterContext.RequestContext.HttpContext.Session["ID"]` – sensei Aug 09 '13 at 01:07
1

Create a custom action filter, and put that code in the filter, then apply the filter to your controller.

Mohammad Sepahvand
  • 17,364
  • 22
  • 81
  • 122
0

You might try implementing your own custom ISessionIDManager: http://msdn.microsoft.com/en-us/library/system.web.sessionstate.isessionidmanager.aspx

In the validate, check to see if it's still valid, otherwise return false.

Robert McKee
  • 21,305
  • 1
  • 43
  • 57