5

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');
  • If I login on 5 different machines, would that not create 5 WP_Session_Tokens with the same login? Also there's a 1 question per question policy, asking multiple questions at once is counterproductive and reduces the chances of quality answers. You can always ask a new question and link here for context. That way someone who knows how to compare the times can answer even if they don't know the solution to the first question – Tom J Nowell Oct 9 '15 at 17:44
  • What do you consider to be "activity" on the site? Is it viewing pages, interacting with specific elements on the public pages, creating/editing content or administering the site, or something else? Is there a room for "low activity" below a threshold, or just no activity at all in that 4 hour timeframe? – Morgan Estes Oct 9 '15 at 20:12
12
+50

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):

  1. A user can have a maximum of 5 login sessions across various browsers and devices.
  2. If more then 5 sessions are attempted it will show an error, unless the oldest activity session is more then 4 hours old.
  3. 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.

  • Do not use this plugin as it states in the GitHub page: "This Plugin Has Serious Security Issue. Please do not use it until further update" – martin schwartz May 28 at 22:40
0

It is impossible to have an answer for the question as it is being asked because http protocol do not have any long term sessions. A session on http is one request and response.

Sessions as we know on the internet are just hacks designed to remove the need to provide login and pass info for every page load.

wordpress 4.1 done a small step in better associating a "session" with an end device, but it is just a better hack than before and it is not 100% reliable as you can still copy the cookies and/or use proxy to foul it into believing that two separate end devices belong to the same session. OTOH it will think that two browser on the same machine are on different devices.

You are just trying to make DRM work, and for the last 20 years there was one thing proven about DRM, 1. It does not prevent people from getting "illegal" access to content, it just takes a little longer. 2. It annoys the paying clients.

Your specific scheme depends on knowing which device is going to be used next, and your assumption that it is the one which was most recently active has no merit. OTOH 5 active session will probably be enough for my whole block, so you don't even "block content stealing"

It is not that the question has no merit at all. I can see something like that used to enhance security, but DRM depends very much on the small details and once you use a vogue but critical term like "active sessions" without defining the details of it first it means for me that you don't really know what you actually expect from the system, as finding out the "active sessions" is the most critical part of implementing your scheme.

  • btw, WP login sessions are saved in usermeta. Not really a PHP session. – Sisir Oct 10 '15 at 17:48
  • yes, I wasn't referring to php sessions, just the general usage of the word "session". – Mark Kaplun Oct 10 '15 at 18:38

protected by Community Nov 2 '17 at 10:54

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.