Skip to content

Conversation

@jnsereko
Copy link
Contributor

@jnsereko jnsereko commented Aug 12, 2021

Hello Everyone, I have tried to improve session management by keeping track of sessions opened by every user before.
When a user changes his password or logs out, all sessions must be invalidated so that an attacker is trapped out.

As @ibacher had advised me, we had to even implement this feature in core but this was almost impossible because the classes I created weren't accessible in the adminui-module.
Thank you
cc @isears @sherrif10 @ibacher

session.management.mp4

@jnsereko
Copy link
Contributor Author

@HerbertYiga
Copy link
Contributor

well it looks like we should have things written to core so that any other module that uses the changes simply picks them from core than depending on legacy ui module

@jnsereko
Copy link
Contributor Author

jnsereko commented Aug 13, 2021

well it looks like we should have things written to core so that any other module that uses the changes simply picks them from core than depending on legacy ui module

Yeah, I agree with you @HerbertYiga. That was my first desire but I ran into issues while trying to access my changes implemented in core. This caused the need to change the OpenmrsVersion used in all modules that need these features. and hence the need to almost change everything in these modules

@jnsereko
Copy link
Contributor Author

Hello @isears @sherrif10 @HerbertYiga @ibacher @dkayiwa everyone is welcome,

I have made some changes to embrace better concurrent functionalities and implement suggestions from Sarah E. Elder
I have tried to dodge changing the interface side of OMRS after a password change. you can view the video below.

Uploading f1f35151-cef0-459f-8af9-b717033ed352.mp4…

How to test this out:

  1. open chrome and login as any user, admin, clerk, nurse ...
  2. open firefox window and/or firefox private browser, and login as the same user.

Below are the test cases i thought about.

  • On password change, all sessions are invalidated and the list of user sessions becomes 0.
  • logging out from one session doesn't affect other existing sessions.

@jnsereko
Copy link
Contributor Author

For this to work fine, you must watch openmrs/openmrs-module-adminui#64

Copy link
Member

@ibacher ibacher left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jnsereko Thanks for this. It's looking like a good outline; just a couple of more nit-picky types of things to clean up and this should be ready-to-go.

import javax.servlet.http.HttpSession;

import org.openmrs.api.context.Context;
import org.openmrs.web.user.CurrentUsers;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This import seems unnecessary.

Comment on lines 113 to 120
List<String> userNames = new ArrayList<String>();
synchronized (currentUsers) {
for (String value : currentUsers.values()) {
for (String value : currentUsers.keySet()) {
userNames.add(value);
}
}
Collections.sort(userNames);
return userNames;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of this seems like unnecessary ceremony.

List<String> userNames = currentUsers.keySet().stream().collect(Collectors.toList());
Collections.sort(userNames);
return userNames; 

}
}
currentUsers.remove(currentUserName);
log.info("Found {} sessions for the user: {}", sessions.size(), currentUserName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These would be better as debug messages

public static Map<String, String> init(ServletContext servletContext) {
Map<String, String> currentUserMap = Collections.synchronizedMap(new TreeMap<String, String>());
public static Map<String, CopyOnWriteArrayList<HttpSession>> init(ServletContext servletContext) {
Map<String, CopyOnWriteArrayList<HttpSession>> currentUserMap = Collections
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few things here:

  1. It's probably better to use a plain HashMap unless we specifically need a sorted map.
  2. It seems a little excessive to have both a synchronized map and a CopyOnWriteArrayList. If we're tracking multiple sessions, a simple ArrayList in a synchronized map is probably fine. Even better would be to just use a plain HashMap and just use a simple lock.

Copy link
Contributor Author

@jnsereko jnsereko Aug 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason why I resorted to CopyOnWriteArryList was that I ran into java.util.ConcurrentModificationException as I was trying to remove a session from the sessions-list while iterating trough all other existing sessions.
This is how it all happened.

private static void invalidateAllUserSessions(String currentUserName,
........	
for (HttpSession session : sessions) {
		if (session != null) {
			session.invalidate();
		}
	}

which calls the method removeSessionFromList through the SessionListener class run on every session invalidation.

@ibacher, Basing on what I have explained above, will option 2 bring out concurrency very well?

Copy link
Member

@ibacher ibacher Aug 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's likely to happen with almost any Java list implementation. You just can't change a list directly while iterating over it. Usually, the way around it it something like this:

Iterator<HttpSession> iterator = sessions.iterator();
while (iterator.hasNext()) {
	HttpSession session = iterator.next();
	session.invalidate();
	iterator.remove();
}

The for (HttpSession session : sessions) bit is actually just syntactic sugar for the first 3 lines of the above example.

Copy link
Contributor Author

@jnsereko jnsereko Aug 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the iterator too before I changed to the CopyOnWriteArryList

Iterator<HttpSession> iterator = sessions.iterator();
while (iterator.hasNext()) {
	HttpSession session = iterator.next();
	session.invalidate();
	iterator.remove();
}

if we use this, the SessionListener will also try to remove the element on session invalidation. In my thinking, I expect some errors here :)

Sometimes sessions are invalidated even when the user hasn't clicked logout. or password change for example when the instance(system) is left unused for like 20 minutes .
My main goal is to use the listener to update the sessions list each time the session is invalidated to prevent having null objects in the list. But this takes place from a different method on a different state of the system.

I don't know if this is clear! But that's how I had understood this.

Copy link
Member

@ibacher ibacher Aug 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the SessionListener will never remove any item from the list. This is because it doesn't know about the list and has no way of removing it. So the list should never contain any null objects.

What it might make sense to do is to instead have a pruneExpiredSessions() method that does something like this:

private static void pruneExpiredSessions(List<HttpSession> sessions) {
	  long currentTimestamp = System.currentTimeMillis();
	  Iterator<HttpSession> iterator = sessions.iterator();
	  while (iterator.hasNext()) {
		  HttpSession session = iterator.next();
		  if (session.getLastAccessTime() - currentTimestamp > session.getMaxInactiveInterval()) {
			  iterator.remove();
		  }
	  }
}

Then after running it:

if (sessions.size() == 0) {
	currentUsers.remove(currentUserName);
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And, yeah, if you're having an issue with an infinite loop of invalidations, then thing to do is to track which session your actually already invalidating, e.g.,

private static void invalidateAllUserSessions(HttpSession httpSession, String currentUserName,
	        Map<String, CopyOnWriteArrayList<HttpSession>> currentUsers) {
		log.info("Finding sessions for the user: {}", currentUserName);

		CopyOnWriteArrayList<HttpSession> sessions = currentUsers.get(currentUserName);
		if (sessions != null) {
			for (HttpSession session : sessions) {
				if (!httpSession.equals(session)) {
					session.invalidate();
				}
			}

			currentUsers.remove(currentUserName);
			log.debug("Found {} sessions for the user: {}", sessions.size(), currentUserName);
		} else {
			log.debug("No sessions found for this user: {}", currentUserName);
		}
	}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And, yeah, if you're having an issue with an infinite loop of invalidations, then thing to do is to track which session your actually already invalidating, e.g.,

Thank you @ibacher for really taking some time to look at this. I am going to polish this and then push the code.

if (log.isDebugEnabled()) {
log.debug("Removing user from the current users. session: " + sessionId + " user: "
+ currentUsers.get(sessionId));
User currentUser = Context.getAuthenticatedUser();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we be getting the user name from the httpSession?

Copy link
Contributor Author

@jnsereko jnsereko Aug 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it a must that every session must have the session-attribute like username set? I honestly don't even know the exact attribute I will be looking for.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely not! I was thinking of looking for a particular session attribute: I was thinking of getting the user by the particular session, e.g.,

public static List<HttpSession> getSessionsForSessionUser(HttpSession httpSession, Map<String, CopyOnWriteArrayList<HttpSession>> currentUsers) {
	for (Map.Entry<String, CopyOnWriteArrayList<HttpSession>> entry : currentUsers.entrySet()) {
		List<HttpSession> sessions = entry.getValue();
		if (sessions != null && sessions.contains(httpSession)) {
			return sessions;
		}
	}

	return Collections.emptyList();
}

if (currentUser != null) {
String userName = getValidUserName(currentUser);
Map<String, CopyOnWriteArrayList<HttpSession>> currentUsers = getCurrentUsers(httpSession);
invalidateAllUserSessions(userName, currentUsers);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually want to invalidate all of the user's sessions when the log out of a single session? I would suggest instead that we simply remove the user from the currentUsers map when the number of sessions remaining is 0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I aimed at invalidating all user sessions on password change. I created this method so that I use it in the adminui-module, and invalidate all user sessions after a password change.

The on logout, the request is always invalidated which calls the listener and the list of sessions is updated

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, I totally buy that we need a method that invalidates all user sessions and that makes a lot of sense. What doesn't make sense to me is killing all of a user's sessions every time one of their sessions expires (this is different from "what happens when I change my password" where it makes sense to kill all of the users sessions). That is to say that the password reset code probably needs to have a special case to invalidate all of the sessions.

Copy link
Contributor Author

@jnsereko jnsereko Aug 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is to say that the password reset code probably needs to have a special case to invalidate all of the sessions.

yeah, sure! @ibacher

In simple terms each time the method removeUser(httpSession) is used, it will invalidate all sessions for the requested user.
Each time a request is invalidated like https://github.com/openmrs/openmrs-module-legacyui/blob/master/omod/src/main/java/org/openmrs/web/servlet/LogoutServlet.java#L46 the Listener is executed and that session is removed from the sessions list of a specified user to prevent references to null objects in the list.

Copy link
Contributor Author

@jnsereko jnsereko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added some changes basing on the comments and discussion i hard with @ibacher
I kindly request for review.

cc @ibacher @dkayiwa @sherrif10 @ibacher

CopyOnWriteArrayList<HttpSession> sessions = currentUsers.get(username);
sessions.remove(httpSession);
log.debug("Removed session: {}. Remaining sessions: {}", httpSession, sessions.size());
System.out.println("Removed session " + httpSession + " Total sessions " + sessions.size());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before we merge this PR, could we get rid of these System.out.println() calls?

Copy link
Contributor Author

@jnsereko jnsereko Sep 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @ibacher
Thank you so much for looking at this. I use System functions to test some cases. Removed it.
Thank you

MM-918: Improved session management logiic

Update omod/src/main/java/org/openmrs/web/user/CurrentUsers.java

Co-authored-by: Ian <[email protected]>

Update omod/src/main/java/org/openmrs/web/user/CurrentUsers.java

Co-authored-by: Ian <[email protected]>

Update omod/src/main/java/org/openmrs/web/user/CurrentUsers.java

Co-authored-by: Ian <[email protected]>

Solved travis, by using getUserName() method

MMM-918: improved session implementation logic

MM-918: Removed System.out functions
@jnsereko
Copy link
Contributor Author

Hello @ibacher @isears @sherrif10
Is this ready for merge?

If yes, may you also take a look at openmrs/openmrs-module-adminui#64

@ibacher ibacher merged commit 79cb4ce into openmrs:master Sep 15, 2021
@ibacher
Copy link
Member

ibacher commented Sep 15, 2021

@jnsereko Thanks for the ping on this. It clearly dropped off my radar. Is there a ticket for this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants