3

When user login I set some information at session:

Session["UserID"] = userUUID;
Session["UserName"] = "John ABC";
Session["UserMail"] = "john@contoso.com";

When user logout I make Session.Abandon() successfully.

But in case an admin need to remove/block that specific user, I need to kill its session if it exists. I currently can prevent that specific user from login again (via database false record).

How can I browse all started session and then have Session.Abandon() for that specific user?

I've tried this post with no success: Get a list of all active sessions in ASP.NET

Please notice we are working with .Net 4.8.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
rd1218
  • 134
  • 12

4 Answers4

1

NOTE: this answer only exists because I've confused ASP.NET MVC 5 (used by the OP) with MVC 6. It uses the newer techniques and hopefully will be useful for some people.


I think you can put that user's ID into application cache and abandon the current session as soon as the server will start handling that user's next request. (Anyway, the consequences of blocking won't manifest themselves until next request.)

To use cache you have to inject IMemoryCache into a couple of controllers. (The namespace is Microsoft.Extensions.Caching.Memory and you need the NuGet package with same name.) If you're not familiar with dependency injection in ASP.NET MVC, please refer here for an example of injecting IMemoryCache. Although the view is for .NET 7.0 (by the way, why don't you upgrade, it's worth that), the same methods were present in .NET 4.8.

To make IMemoryCache available you have to call builder.Services.AddMemoryCache in Program.cs before builder.Build.

So you have injected it into the controller that handles admin's requests. Now let's store there a list of IDs of blocked users:

// cache is of type IMemoryCache and id is of type int (ID of user to block)
// this is an action method

List<int> blockedIds;  // Or List<long> or even List<ulong>
if (!cache.TryGetValue("BlockedIds", out blockedIds)) {
  blockedIds = new List<int>();
}

blockedIds.Add(id);
cache.Set("BlockedIds", blockedIds);

And add a filter that will retrieve the blocked IDs list from cache and abandon the session if current ID (stored in the session) exists there.

UPDATE: here's an example.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class AbandonBlockedSessionsAttribute : FilterAttribute, IActionFilter {
  public void OnActionExecuting(ActionExecutingContext filterContext) {
    IMemoryCache cache = filterContext.HttpContext.RequestServices.GetService<MemoryCache>();
    
    List<int> blockedIds;
    if (cache.TryGetValue("BlockedIds", out blockedIds)) {
      int? id = filterContext.HttpContext.Session.GetInt32("UserID");
      if (id != null && blockedIds.Contains(id)) {
        filterContext.HttpContext.Session.Abandon();
      }
    }
  }

  public void OnActionExecuted(ActionExecutedContext filterContext) { }
}

And register this filter in Program.cs:

GlobalFilters.Filters.Add(new AbandonBlockedSessionsAttribute());
SNBS
  • 671
  • 2
  • 22
  • I just installed from Nuget, I will try to make this work. If you can provide more information, consider that this project does have `Global.asax.cs`. Regarding upgrade from 4.8 to 7.0, it is in your roadmap but for now we are covering several backlogs. – rd1218 Jul 30 '23 at 18:20
  • Again sorry. This won't work for you because I've confused MVC 5 with MVC 6, see my note :( – SNBS Jul 30 '23 at 18:25
  • Thanks for your revised view. The upgrade to 7.0 is in our roadmap, so maybe in the future your clarification may be suitable. – rd1218 Jul 31 '23 at 01:07
0

We had a similar scenario where we wanted to store login information about the current user from the session. However, we opted out of directly storing it on top of the session object, and we detached ourselves from the expiration mechanism of the session, so we only depend on the session ID that the request bears.

What we did was create a custom cache which contains information about the users, identified by the session ID (which is a GUID, thus having astronomically close to 0% chance of collisions). That cache is a ConcurrentDictionary<string, UserInformationModel> instance, with string being the session ID token, and UserInformationModel being a model with all the information a user might have on their current session, including credentials, state, cached profile information, etc.

Note that the above approach closely resembles that of a god model, but it acts as the root of all the information a user bears in their current session.

That dictionary is periodically cleaned with a custom mechanism that iterates through all the current sessions asynchronously and removes the expired entries. We expire entries based on our own rules, they are only alive for 15 minutes since their creation, and never extend the lifetime. Expired entries are never returned if they are expired, even if the session ID is still alive.

We still want to return information about a specific user during the cleanup of the expired entries. We make sure that we never return expired information, and that we can always fall back to re-instantiating a new instance for the same session ID, depending on the expiration state.

To make this all work, and since we do not store any information on the Session object of the HttpContext, we store a dummy value set to 1 just to cause ASP.NET Core to preserve the Session object and its ID. In the documentation it notes that if the session carries no information, it is immediately discarded, hence the above workaround.

Thus, we have a system that relies on the automatically generated session IDs of ASP.NET Core, but store our information in a custom in-memory cluster that cleans itself up periodically and prevents invalidated data from being used.

For your case, you might try some similar structure out. It will definitely help since you are storing several fields about a user, which user you effectively identify via a session object.

Also, the greatest deal of all this is that you have a root object that provides access to the fields of your interest without knowing about the names of the fields in a name-value dictionary. It helps you be certain that those fields are not ghosts and have a commonly shared model across the entirety of your application that involves user information.

P.S. You mention you use .NET Framework 4.8, thus not ASP.NET Core. However, the same approach can be easily applied in ASP.NET, as you are still provided with a session object on the HttpContext object of each request you are handling.

Rekkon
  • 177
  • 12
  • We are currently working on something like you described, which is also somehow mentioned in the two other answers provided so far. When we finish, I may publish here for future reference. Thanks for your thoughs on this. – rd1218 Aug 01 '23 at 23:47
0

Existing answers didn't exactly answer my question, but with the help of a senior team mate those answers provided insights to us. See below the detailed steps we made.

Please notice that this project runs on .Net 4.8 and is a MVC 5. In newer versions this is smoother to implement.

First we installed Microsoft.Extensions.Caching.Memory from Nuget, version 7.0.0 that runs fine on current .Net 4.8 project version. It also installed some required dependencies. This package creates a "place" where different sessions can access and read/write to.

On UnityConfig.cs the lines below were added. Here we set the logged list containing UserSession objects, which has information that we need (UserID, UserName, CompanyName and others).

var memoryCache = new MemoryCache(new MemoryCacheOptions() {  });
var listLoggedUsers = new List<UserSession>();
memoryCache.Set("logged", listLoggedUsers);
container.RegisterInstance<IMemoryCache>(memoryCache, InstanceLifetime.Singleton);

To access this package from within the controller:

public class AccountController : RootController
{
    private readonly IMemoryCache _memoryCache;
    public AccountController(IMemoryCache memoryCache)
    {
        _memoryCache = memoryCache;
    }
     //Code here
}

To access this package from within a class:

var memoryCache = DependencyResolver.Current.GetService<IMemoryCache>();

When the user login to system, their information is stored in HttpContext.Current.Session and also at memoryCache shown above. So, at AccountController we have this code:

public ActionResult UserLogin( ... )
{
    //Initial code here

    //Saves to session
    Session["UserID"] = userUUID;
    Session["UserName"] = "John ABC";
    Session["UserMail"] = "john@contoso.com";

    //Saves to memoryCache
    _memoryCache.TryGetValue("logged", out List<UserSession> listLoggedUsers);
    var user = new UserSession()
    {
        //Object properties, with `userUUID` the identifier
    };
    listLoggedUsers ?? = new List<UserSession>();
    listLoggedUsers.Add(user);
    _memoryCache.Set("logged", listLoggedUsers);

    //Final code here
}

The above code is very similar to everywhere else we applied memoryCache.

To read something from it (find userUUID):

_memoryCache.TryGetValue("logged", out List<UserSession> listLoggedUsers);
if (listLoggedUsers != null)
{
    var sessionData = listLoggedUsers.FirstOrDefault(p => p...);    
    if (sessionData != null)
    {
        //Do something
    }
}

To update its contents:

_memoryCache.TryGetValue("logged", out List<UserSession> listLoggedUsers);
foreach (var sessionData in listLoggedUsers)
    if (sessionData == desired_information)
        //Do something
_memoryCache.Set("logged", listLoggedUsers);

You can also store additional information by creating different lists. In the example above it was logged list, but others can also be created. It's important to have a look at their documentation because RAM availability may be an issue.

rd1218
  • 134
  • 12
-1

Im assuming that you are on InProc Session mode. There is no way to manage another session from the current session, you can use a flag in the application that can be checked on every request and when that flag is set to true it can be "abandoned". A simple implementation could be:

1- At application level set the user on the app session.

Application["AbandonSession"] = new List<string> (); // add null check to initialize once
Application["AbandonSession"]).Add(theUser);

2- Implement BeginRequest on global.asax.cs

protected void Application_BeginRequest(object sender, EventArgs e)
{
    if (((List<string>) Application["AbandonSession"]).Contains(theUser))
    {
        // Whatever you need to do


        Session.Abandon();//lastly call Abandon for theUser
    }
}
jmvcollaborator
  • 2,141
  • 1
  • 6
  • 17
  • Step 1 initalization would be at "Application_Start" (therefore available to all sessions)? Step 1 "Add(theUser)" would occur at "ActionResult BlockUserFromSystem(Guid userID)"? At step 2 I understood that "theUser" would be "userID" that I would retrieve from Session["UserID"], is that correct? Please clarify. – rd1218 Jul 30 '23 at 02:47
  • If step 1 "Add(theUser)" occur at "ActionResult BlockUserFromSystem(Guid userID)", please clarify on how to achieve this. If I try "Application["AbandonSession"].Add(theUser)" I get and error "The name Application does not exist in the current context". – rd1218 Jul 30 '23 at 03:02
  • @SNBS I do have `Global.asax.cs` in my project. To answer your question I did some research and concluded that it is MVC 5. – rd1218 Jul 30 '23 at 18:12
  • Oh I'm really sorry, I've confused MVC 5 with MVC 6. @jmvcollaborator please edit your answer so that I could remove downvote (if you care). – SNBS Jul 30 '23 at 18:23
  • 1
    no worries i will delete it, thanks for taking care of ur vote – jmvcollaborator Jul 30 '23 at 23:47