in Codeigniter I am building an Authentication system for my web site and to achieve that I use session library
session->set_userdata('username')
this will save the session -I believe- for some time
I want to provide a "remember me" checkbox in the login form so the user can save the session forever - could not find a way to save the session forever!?
Note:$sess_expiration will not work because it sets expiration date for all users and what I want to do is setting the expiration date based on his preferences
is that possible? and how to do it?
Thanks
If the remember me checkbox is checked you set a cookie on the user's system with a random string. E.g.:
$cookie = array(
'name' => 'remember_me_token',
'value' => 'Random string',
'expire' => '1209600', // Two weeks
'domain' => '.your_domain.com',
'path' => '/'
);
set_cookie($cookie);
You also save this random string in the users table, e.g. in the column remember_me_token.
Now, when a user (who is not yet logged in) tries to access a page that requires authentication:
you check if there is a cookie by the name of remember_me token on his system
if it's there, you check the database if there is a record with the same value
if so, you recreate this user's session (this means they are logged in)
show the page they were visiting
If one of the requirements above is not met, you redirect them to the login page.
For security reasons you may want to renew the random remember_me_token every time the user logs in. You can also update the expiry date of the cookie every time the user logs in. This way he will stay logged in.
It would be too much work to write all the code for you, but I hope this helps you to implement this functionality yourself. Please comment if you have any questions. Good luck.
I needed the same thing. You cannot accomplish this using CI settings so I have chosen to override the setcookie method of the CI Session class (in MY_Session):
function _set_cookie($cookie_data = NULL)
{
if (is_null($cookie_data))
{
$cookie_data = $this->userdata;
}
// Serialize the userdata for the cookie
$cookie_data = $this->_serialize($cookie_data);
if ($this->sess_encrypt_cookie == TRUE)
{
$cookie_data = $this->CI->encrypt->encode($cookie_data);
}
else
{
// if encryption is not used, we provide an md5 hash to prevent userside tampering
$cookie_data = $cookie_data.md5($cookie_data.$this->encryption_key);
}
setcookie(
$this->sess_cookie_name,
$cookie_data,
$this->userdata('rememberme') == true ? $this->sess_expiration + time() : 0,
$this->cookie_path,
$this->cookie_domain,
0
);
}
Of course you need to set the rememberme flag in your session based upon the choice the user made.
I used hooks to do this
Enable hooks in "config.php" setting $config['enable_hooks'] = TRUE;
In your login controller save the checkbox value on session
$this->session->set_userdata('remember_me', $this->input->post('remember_me'));
In your "hooks.php" file add this
$hook['post_controller_constructor'] = array(
'class' => 'Change_Config',
'function' => 'change_session_expiration',
'filename' => 'change_config.php',
'filepath' => 'hooks'
);
In your "hooks" folder create a file called "change_config.php" and paste this
<?php
class Change_Config {
public function change_session_expiration() {
$ci = & get_instance();
$remember = $ci->session->userdata('remember_me');
if($remember && $ci->session->sess_expire_on_close) {
$new_expiration = (60*60*24*365); //A year milliseconds
$ci->config->set_item('sess_expiration', $new_expiration);
$ci->config->set_item('sess_expire_on_close', FALSE);
$ci->session->sess_expiration = $new_expiration;
$ci->session->sess_expire_on_close = FALSE;
}
}
}
You could use isset to check if the cookie has a value, and if the cookie is deleted, it will return negative. If it is negative, you can set the cookie again... of course, this will be like "renewing" the cookie, so if the user doesn't come by the expiration, they will be forgotten. (Set a very big time till the expiration!)
For example:
<?php
if (isset($_COOKIE['autologin'])) { //Checks for cookie
echo "Welcome back!... Logging you in.";
}else{ //Sets a cookie
echo "Please sign in";
$expiretime=time()+60*60*24*365; //set expire time to a year
setcookie("autologin", "What is here is not important.", $expiretime);
};
?>
$this->session->sess_expiration = '14400'; // expires in 4 hours
$this->session->set_userdata($data); // set session
You can just put this
if ($remember) {
$new_expiration = (60*60*24*365);
$this->config->set_item('sess_expiration', $new_expiration);
$this->config->set_item('sess_expire_on_close', FALSE);
$this->session->sess_expiration = $new_expiration;
$this->session->sess_expire_on_close = FALSE;
}
You can set sess_expiration to 0 and then set a custom variable into the session like, remember_me=1. Check this value and destroy the session if needed after a time.
This would be a workarround.
Related
First off let me know if this is not the correct place for this discussion!
We have decided to move our sessions to the Database layer in our application and handle the users state from there. One of the important issues that we hoped to resolve was user authentication to stop from brute forcing accounts by blocking login attempts from both the session layer (built from a database entry) as well as on the account layer - with an optional toggle to remove the account specific lockout for users as the potential remains for bad actors to then attempt to lock a legitimate user out of their account by spamming the account login - but the tradeoff here is then the account cannot be brute forced. This may be a toggle for specific accounts in the future if we decide the build the app around that idea more.
The current script for sessions includes a function (which i have included below) that you can call on the login event called check_login_attempts_exceeded and will return TRUE if they have been or FALSE if they have not been - Allowing you to either accept the login attempt, or block the attempt before it even hits the login server.
Basically, the user flow would work something like this:
Authenticated user::
We will just return FALSE right away because they are already logged in and don't
need to know this information on their own account. It does allow for them to check
other users with different user ids since they may be an admin and have a need for
that information? This is primarily designed for Guest logins anyways before the
user has been Authenticated in the system.
Spider::
Treat them as a guest account so the spider mechanism isn't exploited by people
to try and bypass the users login limit.
Guest account::
1) The system confirms they are a guest and continues, or returns FALSE if they
are already signed in to an account matching the ID of their user id -> this is
the Authenticated users section above.
2) Check if the user has a login lockout flag set on the SESSION. We don't care
about specific users **yet**, but we will check them afterwards.
NO::
1). Check if the user is at the max attempts in the system:
YES::
1). If the user is at the maximum attempts but doesn't have the flag set
for some reason, we add the login_lockout globally and add it to the
users SESSION in the database.
2). Return TRUE so the system knows they've exceeded max attempts
NO::
1). If the user has a supplied ID greater than 0, they can check if a
specific account is over. Since the flag is not yet set, this is the
only time this can be checked for specific accounts. This will be a
time code on the ACCOUNT level and if it is passed we handle it [1]
way, and if it's not passed we handle it [2] way.
[1]-> The lockout time on the ACCOUNT level is still in the future
so we are going to return TRUE because this account level check
shows the account is locked regardless of this SESSION and we
want to prevent brute forcing of accounts. The downside to this
method is a specific user can be locked out of their accounts
legitimately if a bad actor spams their login name.
[2]-> The lockout on the ACCOUNT level is in the past, so lets set
the login attempts and lockout time on the ACCOUNT level to 0
for both and return FALSE since the timeout is no longer on
and the account hasn't exceeded it's limit! :)
2). The user id wasn't supplied so we assume the user hasn't gone over
yet because the flag isn't set yet by any previous condition and the
lockout time doesn't exist either so we return FALSE :)
2). Return FALSE as a failsafe incase the user id wasn't set and they are
not at the limit meaning they've passed the test and not exceeded attempts
YES::
1). The user has the flag set so lets see if the timeout on the flag has
expired and remove it if it has
YES::
1). Remove the flag, update the session and reset the attempts and the
lockout time to 0 again. Ideally return FALSE now, but we need to
check the ACCOUNT specific timeout as well just in case that one is
still set.
The last portion is the bit I am wanting to be sure of. Basically, if the user HAS a time flag on their SESSION, we need to check if its expired and remove it if it is - but then the ACCOUNT layer inside of this may be time locked separately so we need to check that, remove it if its expired, or return as invalid if its not expired.
I am doing this with the following code (PHP) and have commented as much of the code as possible to explain the steps and where I am at:
// Check if the user has exceeded their login attempts, and if they are locked
// out return TRUE, and if the lockout has expired, remove it from the session
// AND the user account if it is set - Optional return time to unlock
function check_login_attempts_exceeded($database, $config, $userid = 0, $return_time = FALSE) {
// Set the time of now for checking if they are past the lockout expiry
$time = CURRENT_TIME;
$userid = (int) $userid;
$unauthenticated_user = FALSE;
// Check if this user is logged in - If they are, we can return false
// since this user is already logged in and doesn't need to have attempts
// logged for them anymore.
if ($userid > 0 && $userid == $this->userid) {
return FALSE;
} else {
$unauthenticated_user = TRUE;
}
// If the user is a guest, we can process the request since it's not important
// otherwise. This condition should always be true, but better safe than sorry?
if ($unauthenticated_user) {
// If the login_timeout isn't set we can check if the attempts are over
// and set it otherwise, it must be set so we will see if its expired
if (!$this->login_timeout || $this->login_timeout == 0) {
// Check the global lockout first - if it above the max then we will
// ignore the fact that the user is over since we're not there yet
if ($this->login_attempts >= $config->general->max_login_trys) {
$this->login_timeout = (int) $time + ($config->general->lockout_time * 60);
$database->update_prepared_query("sessions", array("lockout_time" => $this->login_timeout), array("sid" => $this->session_id));
return TRUE;
}
// Now lets check the specific user case, because we know that the
// above has been handled and returned already
if ($userid > 0) {
$user_info = $database->prepared_select("users", "WHERE uid = ? LIMIT 1", array($userid), "loginattempts, loginlockoutexpiry");
if ($user_info) {
$user_specific_loginattempts = $user_info["loginattempts"];
$user_specific_loginexpiry = $user_info["loginlockoutexpiry"];
// If this specific user is over the max attempts, then we return
// true because they're over the max attempt
if ($user_specific_loginattempts >= $config->general->max_login_trys) {
// We also want to see if their expiry is over the max limit
// and it if is, we will return true, if it is expired,
// lets remove it and set these back to 0 and return false!
if ($user_specific_loginexpiry > $time) {
if ($return_time) {
$secsleft = (int) ($user_specific_loginexpiry - $time);
$hoursleft = floor($secsleft / 3600);
$minsleft = floor(($secsleft / 60) % 60);
$secsleft = floor($secsleft % 60);
return array("hours" => $hoursleft, "minutes" => $minsleft, "seconds" => $secsleft);
}
return TRUE;
} else {
// This user specific timeout has expired, so lets
// remove it from the system and let the user attempt
// a login!
$database->update_prepared_query("users", array("loginattempts" => 0, "loginlockoutexpiry" => 0), array("uid" => $userid));
return FALSE;
}
}
} else {
// There was no user information found for this id, so we can't
// return a result and we will just return false instead
return FALSE;
}
}
// Must not be over yet then, so lets return false as no user was
// supplied and the user hasn't hit the limit globally yet either
return FALSE;
} else {
// The login timeout is set, let see if it's expired or not?
if ($this->login_timeout <= CURRENT_TIME) {
if ($userid > 0) {
$user_info = $database->prepared_select("users", "WHERE uid = ? LIMIT 1", array($userid), "loginattempts, loginlockoutexpiry");
if ($user_info) {
if ($user_info["loginattempts"] >= $config->general->max_login_trys) {
if ($user_info["loginlockoutexpiry"] > $time) {
return TRUE;
} else {
$this->login_timeout = 0;
$this->login_attempts = 0;
$database->update_prepared_query("sessions", array("login_attempts" => $this->login_attempts, "lockout_time" => $this->login_timeout), array("sid" => $this->session_id));
$database->update_prepared_query("users", array("loginattempts" => 0, "loginlockoutexpiry" => 0), array("uid" => $userid));
return FALSE;
}
} else {
// The user they're trying for is not at the max tries so we can return false!
return FALSE;
}
} else {
// There was no user information found for this id, so we can't
// return a result and we will just return false instead
return FALSE;
}
} else {
// The login timeout has expired for this guest account so
// we remove it globally!
$this->login_timeout = 0;
$this->login_attempts = 0;
$database->update_prepared_query("sessions", array("login_attempts" => $this->login_attempts, "lockout_time" => $this->login_timeout), array("sid" => $this->session_id));
return FALSE;
}
}
// They must still be expired, so lets return true!
return TRUE;
}
}
return FALSE;
}
Does this seem like a good userflow and does the function seem to be missing anything that you would suggest that could potentially lead to an error?
I have been troubleshooting it and all my conditions SEEM to be passing, but I've also spent about 100 hours on this application already and a fresh pair of eyes to suggest any holes I may have missed would be very beneficial at this point.
If it all looks good, let me know so I can stop going crazy overbuilding this thing!
i just force expire the cookie it's quite simple its balance between being secure and provably annoying for brute force
<?php
session_start();
session_regenerate_id(true); // regenerate new cookie everytime page refresh
$reload_userTimeout=40; // set the new cookie to expire 40seconds everytime user reload the page( they need to type the form in just 40 secs)
setcookie(session_name(),session_id(),time()+$reload_userTimeout);
if (login detail wrong) {
session_unset();
session_regenerate_id(true); // regenerate new cookie
$captchaWrong_userTimeout=15; // since they got login details wrong The cookie would now expire 15 secs ( shorter than before so they need to type fast as possible :p )
setcookie(session_name(),session_id(),time()+$captchaWrong_userTimeout);
} else { // login detail is correct
session_unset();
session_regenerate_id(true); // regenerate new cookie
$captchaCorrect_userTimeout=1; // force expire the cookie in just 1 second
setcookie(session_name(),session_id(),time()+$captchaCorrect_userTimeout);
session_write_close(); // prevent cookie from being overwritten ( user must clear web browser cookies or use incognito mode to overwrite the cookie )
}
?>
I am trying to make when non auth user click to link "Add To wishes" these link going to addtowishes method and send parameter id.
After, first I check does cookie 'wishes' exist if not, I make empty array and in that array I add 'id' after I push this array in cookie.
in else case I get cookie 'wishes' and in that array I push id.But every time when i start method, cookie::get('wishes') is null.
public function addtowishes($id)
{
if(emptyArray(Cookie::get('wishes'))) {
$wishes=Array();
array_push($wishes,$id);
$cookie = cookie('wishes', $wishes, 500);
}else {
$cookie=Cookie::get('wishes');
array_push($cookie->getValue(), $id);
$cookie = cookie('wishes', $cookie->getValue(), 500);
dd($cookie);
}
}
Cookies are set with the function setcookie()
also note that the expire parameter is a unix timestamp. therefore 500 will always be expired. Use time()+60*60*24*30 for 30 days for example
What I've been trying is to set a token (i.e. a string) as a cookie, which needs to persist for a certain period.
public function tokenAuth(Request $request)
{
try {
//Lines which create a token from input values
} catch (MyCustomException $e) {
//Return with a warning message
}
$minutes = time() + 60 * 60 * 24 * 30; //time()+60*60*24*30 = 30 days
$https = false;
$httpOnly = true;
return response('Hello World')->cookie("MY_JWT", $token, $minutes, '/', env('APP_URL'), $https, $httpOnly);
}
As long as valid credentials are provided, this tokenAuth is supposed to put a cookie MY_JWT. I can see that MY_JWT is generated properly from Developer Tools.
However, as the title suggests, MY_JWT disappears upon refreshing. According to Laravel's documentation, cookies can be generated in this way.
I also tried setcookie('cookie_id', $id, 0, '/'), referring to this, but I still have no luck.
A funny thing I found is that XSRF-TOKEN is in the cookies, although I haven't put csrf_token in my app.
This is what happens after I refresh the page.
The cookie named blah was created several days ago for an experimentation.
Do you see anything I'm doing wrong?
Any advice will be appreciated!
How can I tell yii2 to logged out specific user who has login to system?
Let say, there is 2 user is logged in on system, userA and userB.
How can I specified logged out userB?
What I know is userB has to trigger this command to logout.
Yii::$app->user->logout();
return $this->goHome();
But maybe we can use some command like Yii::$app->user->logout('userB');?
Or is it has some other way?
Well, the problem is about how to kill all sessions of the user.
Flag user to force relogin
You can add an additional column force_relogin to User identity class and set it to true when you need to logout someone:
$user = User::findByLogin('userB');
$user->force_logout = 1;
$user->save();
Then add the event handler beforeLogin() on user component like so:
'user' => [
'class' => 'yii\web\User',
'on beforeLogin' => function ($event) {
if ($event->identity->force_logout && $event->cookieBased) {
$event->isValid = false;
}
},
'on afterLogin' => function ($event) {
if ($event->identity->force_logout) {
$event->identity->force_logout = false;
$event->identity->save();
}
}
]
Check, whether $cookieBased and $identity->force_logout are so on...
But that's a bad idea, because the user may have more than one session (logged in in different browsers)
Store list user's sessions in DB
Create table user_sessions with user_id and session_id columns and save each session, you open for that user in the DB. That you can find all sessions of the user and drop them one by one. Something like: (code is not tested, just as an idea)
$sessionId = Yii::$app->session->getId();
session_commit();
foreach (UserSessions::findByUserLogin('userB') as $session) {
session_id($session->id);
session_start();
session_destroy();
session_commit();
}
session_id($sessionId); // Restore original session
session_start();
session_commit();
The idea is weak because you always should take care about consistence of sessions on the server and in the DB.
Store sessions in DB
Store sessions is the database, as described in the Yii2 Guide for Session handling
Then you can just find session in the DB and delete it directly. You shouldn't take care about the consistence and session rotation, because DB is the only place, where the sessions are being stored. As a free bonus you get a non-blocking work with sessions.
If you are still looking for a solution to this, just change the auth_key of the user that you want to logout. This auth_key is used by the system to remember if a user is logged in, thus changing this will invalidate any session that uses this key.
Example to logout a user with id=100
$user = User::findOne(100);
$user->generateAuthKey(); //a function in User (Identity) class that generates the auth_key
$user->save();
If you are using a model User for storing auth credentials, you can simply change the value of 'id' (and also a key '100') to some other integer value, to make user fail auth on his next request.
In other words, you must change all '100', for example to '200' in this code:
file: /models/User.php
private static $users = [
'100' => [
'id' => '100',
'username' => 'admin',
'password' => 'password_for_admin',
'authKey' => '43e02a0f0e5f6a7907b2f2c69a765be7',
'accessToken' => '7b2f2c69a765be743e02a0f0e5f6a790',
],
];
I have a concern about Zend Auth. I've searched throughout the internet, but haven't found any tutorial, article or discussion;
scenario: Login as 'ADMIN' on 'COMPUTER 01' and 'COMPUTER 02' concurrently.
I want my user login system to prevent the 'ADMIN' user from logging in on two computers at the same time. So that when a User is already logged in, the system disables login on another machine.
As far as I am aware this functionality is not built in to Zend_Auth, but you could achieve what you want by extending the Zend_Auth_Adapter that you are currently using and overiding the authenticate() method as danielrsmith suggests.
You would need to add a table to your DB that is set/unset by the login process. The problem is going to be the unset if the user does not specifically log out, but you could store a timestamp in the DB and allow the login to expire for the next login attempt.
My_Auth_Adapter_DbTable extends Zend_Auth_Adapter_DbTable
{
public function authenticate()
{
$authResult = parent::authenticate();
if($this->alreadyLoggedIn(){
$authResult = new Zend_Auth_Result(
Zend_Auth_Result::FAILURE_UNCATEGORIZED,
$this->_identity,
array('User already logged in')
);
} else {
$this->setLoggedIn();
}
return $authResult;
}
private function alreadyLoggedIn()
{
//check db table to see if $this->_identity is already logged in
//then return true or false as appropriate
}
private function setLoggedIn()
{
//update table in DB to reflect logged in status
}
}
I haven't tested this code, but it will, hopefully, get you started in the right direction.
Also, doing it this way will, I think, avoid the need to alter the session handler in any way.
Try this:
Create session table in database:
CREATE TABLE session (
id char(32),
modified int,
lifetime int,
data text,
PRIMARY KEY (id)
);
Store session in database by Zend_Session_SaveHandler_DbTable. Put the following code in bootstrap.php
protected function _initDoctrineSession()
{
$url=constant("APPLICATION_PATH").DIRECTORY_SEPARATOR ."configs".DIRECTORY_SEPARATOR."application.ini";
$config=new Zend_Config_Ini($url,"mysql");
$db=Zend_Db::factory($config->db);
Zend_Db_Table_Abstract::setDefaultAdapter($db);
$config = array(
'name' => 'session',
'primary' => 'id',
'modifiedColumn' => 'modified',
'dataColumn' => 'data',
'lifetimeColumn' => 'lifetime'
);
Zend_Session::setSaveHandler(new Zend_Session_SaveHandler_DbTable($config));
Zend_Session::start();
}
When log in, save user id or user name in session:
$logged_user = new Zend_Session_Namespace('logged_user');
$logged_user->logged_user = $user->name;
Another log in deletes all expired sessions in database firstly:
$sessionModel = new Session();
$sessionModel->removeExpiredSessions();
5.After log in, search session table records to see if current user already logged in:
$sessions = $sessionModel->querySessions();
$flag=true;
foreach ($sessions as $session){
if(strpos($session['data'], $user->name)>0 and strpos($session['data'],"Zend_Auth")>0
){
$flag=false;
}
}
if($flag == false){
$this->_forward('index','index');
return;
}
This will work. But there is a problem, if a user closes the Web browser before log out, the user will not be able to log in again before the session expired. Anyone can help to fix the last problem?