I’m having trouble implementing a concurrent login check.
The site needs to prevent more than 5 concurrent sessions for any particular user at one time.
Example:
User Matt can have 5 active sessions.
If user Matt tries to login with a 6th session, it will remove the session which logged in first & had no activity older than 4 hours.
If all 5 sessions have had activity in the past 4 hours, login fails and the user is presented an error/message to contact site admin.
I know WordPress has WP_Session_Tokens but it seems they only store ‘expiration’ and ‘login’ with no ‘last_activity’. Is there any way to check for last activity either through WordPress or PHP Sessions?
If not then a secondary question of mine is how best to compare ‘last’ login to current time and check if it’s more than 4 hours.
Here is my current code:
// On login, check if there are already 5 sessions active for the user
function check_sessions($login) {
global $user_ID;
$user = get_user_by( 'slug', $login );
//If there are less than 5 sessions, let user login normally
if( count( wp_get_all_sessions() ) < 5 ) {
return true;
}
$sessions = WP_Session_Tokens::get_instance( $user->id );
$all_sessions = $sessions->get_all();
$first_login = $all_sessions[0]['login'];
if( $first_login->diff(time()) > 4hrs ) {
// log out first_login user & login new user
WP_Session_Tokens::destroy( $all_sessions[0] );
return true;
}
else {
// display message to user
}
}
add_action('wp_login','check_sessions');
2 s
This question made me really interested. Took about 5 hours of my Saturday to create the full solution 🙂
Plugin Limit Login Sessions
It doesn’t provide a settings page yet, so all options are currently hard coded. The plugin implements the following (according to OP):
- A user can have a maximum of 5 login sessions across various browsers and devices.
- If more then 5 sessions are attempted it will show an error, unless the oldest activity session is more then 4 hours old.
- If the oldest activity session is more then 4 hours old, that session will be closed and current attempt of the login is allowed.
I tried to add explanations in the code with comments. Most of the plugin code should be self explanatory. If some part of it is not clear, feel free to comment.
The GitHub repository can be found here. Feel free to fork and improve it 🙂 If anyone thinks it would be a useful addition to the WordPress plugin repository, let me know and I will upload to WordPress.org if required.
<?php
/*
Plugin Name: Limit Login Sessions
Version: 1.0.0
Author: Sisir Kanti Adhikari
Author URI: https://sisir.me/
Description: Limits users login sessions.
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
Limit Login Sessions is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
any later version.
Limit Login Sessions is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License (http://www.gnu.org/licenses/gpl-2.0.html)
for more details.
*/
add_filter('authenticate', 'lls_authenticate', 1000, 2);
function lls_authenticate($user, $username){
if(!username_exists($username) || !$user = get_user_by('login', $username))
return null; // will trigger WP default no username/password matched error
// setup vars
$max_sessions = 5;
$max_oldest_allowed_session_hours = 4;
$error_code="max_session_reached";
$error_message = "Maximum $max_sessions login sessions are allowed. Please contact site administrator.";
// 1. Get all active session for this user
$manager = WP_Session_Tokens::get_instance( $user->ID );
$sessions = $manager->get_all();
// 2. Count all active session
$session_count = count($sessions);
// 3. Return okay if active session less then $max_sessions
if($session_count < $max_sessions)
return $user;
$oldest_activity_session = lls_get_oldest_activity_session($sessions);
// 4. If active sessions is equal to 5 then check if a session has no activity last 4 hours
// 5. if oldest session have activity return error
if(
( $session_count >= $max_sessions && !$oldest_activity_session ) // if no oldest is found do not allow
|| ( $session_count >= $max_sessions && $oldest_activity_session['last_activity'] + $max_oldest_allowed_session_hours * HOUR_IN_SECONDS > time())
){
return new WP_Error($error_code, $error_message);
}
// 5. Oldest activity session doesn't have activity is given recent hours
// destroy oldest active session and authenticate the user
$verifier = lls_get_verifier_by_session($oldest_activity_session, $user->ID);
lls_destroy_session($verifier, $user->ID);
return $user;
}
function lls_destroy_session($verifier, $user_id){
$sessions = get_user_meta( $user_id, 'session_tokens', true );
if(!isset($sessions[$verifier]))
return true;
unset($sessions[$verifier]);
if(!empty($sessions)){
update_user_meta( $user_id, 'session_tokens', $sessions );
return true;
}
delete_user_meta( $user_id, 'session_tokens');
return true;
}
function lls_get_verifier_by_session($session, $user_id = null){
if(!$user_id)
$user_id = get_current_user_id();
$session_string = implode(',', $session);
$sessions = get_user_meta( $user_id, 'session_tokens', true );
if(empty($sessions))
return false;
foreach($sessions as $verifier => $sess){
$sess_string = implode(',', $sess);
if($session_string == $sess_string)
return $verifier;
}
return false;
}
function lls_get_oldest_activity_session($sessions){
$sess = false;
foreach($sessions as $session){
if(!isset($session['last_activity']))
continue;
if(!$sess){
$sess = $session;
continue;
}
if($sess['last_activity'] > $session['last_activity'])
$sess = $session;
}
return $sess;
}
// add a new key to session token array
add_filter('attach_session_information', 'lls_attach_session_information');
function lls_attach_session_information($session){
$session['last_activity'] = time();
return $session;
}
add_action('template_redirect', 'lls_update_session_last_activity');
function lls_update_session_last_activity(){
if(!is_user_logged_in())
return;
// get the login cookie from browser
$logged_in_cookie = $_COOKIE[LOGGED_IN_COOKIE];
// check for valid auth cookie
if( !$cookie_element = wp_parse_auth_cookie($logged_in_cookie) )
return;
// get the current session
$manager = WP_Session_Tokens::get_instance( get_current_user_id() );
$current_session = $manager->get($cookie_element['token']);
if(
$current_session['expiration'] <= time() // only update if session is not expired
|| ( $current_session['last_activity'] + 5 * MINUTE_IN_SECONDS ) > time() // only update in every 5 min to reduce db load
){
return;
}
$current_session['last_activity'] = time();
$manager->update($cookie_element['token'], $current_session);
}
For some functionality, I had to directly interact with the database user_meta
value. The class had some methods protected, so couldn’t be accessed directly.
Plugin is tested locally with WP v4.3.1
.