Merge pull request #136 from maxxer/password_expiration
Password expiration
This commit is contained in:
@ -6,6 +6,7 @@
|
|||||||
- Bug #133: Fix user search returning no results in admin page (phiurs)
|
- Bug #133: Fix user search returning no results in admin page (phiurs)
|
||||||
- Bug #125: Fix validation in non-ajax requests (faenir)
|
- Bug #125: Fix validation in non-ajax requests (faenir)
|
||||||
- Bug #122: Fix wrong email message for email address change (liviuk2)
|
- Bug #122: Fix wrong email message for email address change (liviuk2)
|
||||||
|
- Bug #102: Implemented password expiration feature (maxxer)
|
||||||
|
|
||||||
## 1.1.1 - November 27, 2017
|
## 1.1.1 - November 27, 2017
|
||||||
- Bug #115: Convert client_id to string because pgsql fail with type convertion (Dezinger)
|
- Bug #115: Convert client_id to string because pgsql fail with type convertion (Dezinger)
|
||||||
|
|||||||
@ -48,6 +48,25 @@ If `true` it will enable password recovery process.
|
|||||||
If `true` and `allowPasswordRecovery` is false, it will enable administrator to send a password recovery email to a
|
If `true` and `allowPasswordRecovery` is false, it will enable administrator to send a password recovery email to a
|
||||||
user.
|
user.
|
||||||
|
|
||||||
|
#### maxPasswordAge (type: `integer`, default: `null`)
|
||||||
|
|
||||||
|
If set to an integer value it will check user password age. If the days since last password change are greater than this configuration value
|
||||||
|
user will be forced to change it. This enforcement is done only at login stage. In order to perform the check in every action you must configure
|
||||||
|
a filter into your controller like this:
|
||||||
|
```
|
||||||
|
use Da\User\Filter\PasswordAgeEnforceFilter;
|
||||||
|
class SiteController extends Controller
|
||||||
|
{
|
||||||
|
public function behaviors()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
[...]
|
||||||
|
'enforcePasswordAge' => [
|
||||||
|
'class' => PasswordAgeEnforceFilter::className(),
|
||||||
|
],
|
||||||
|
```
|
||||||
|
This will redirect the user to their account page until the password has been updated.
|
||||||
|
|
||||||
#### allowAccountDelete (type: `boolean`, default: `true`)
|
#### allowAccountDelete (type: `boolean`, default: `true`)
|
||||||
|
|
||||||
If `true` users will be able to remove their own accounts.
|
If `true` users will be able to remove their own accounts.
|
||||||
|
|||||||
@ -25,6 +25,10 @@ use yii\console\Application as ConsoleApplication;
|
|||||||
use yii\i18n\PhpMessageSource;
|
use yii\i18n\PhpMessageSource;
|
||||||
use yii\web\Application as WebApplication;
|
use yii\web\Application as WebApplication;
|
||||||
|
|
||||||
|
use yii\base\Event;
|
||||||
|
use Da\User\Event\FormEvent;
|
||||||
|
use Da\User\Controller\SecurityController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bootstrap class of the yii2-usuario extension. Configures container services, initializes translations,
|
* Bootstrap class of the yii2-usuario extension. Configures container services, initializes translations,
|
||||||
* builds class map, and does the other setup actions participating in the application bootstrap process.
|
* builds class map, and does the other setup actions participating in the application bootstrap process.
|
||||||
@ -91,6 +95,7 @@ class Bootstrap implements BootstrapInterface
|
|||||||
// services
|
// services
|
||||||
$di->set(Service\AccountConfirmationService::class);
|
$di->set(Service\AccountConfirmationService::class);
|
||||||
$di->set(Service\EmailChangeService::class);
|
$di->set(Service\EmailChangeService::class);
|
||||||
|
$di->set(Service\PasswordExpireService::class);
|
||||||
$di->set(Service\PasswordRecoveryService::class);
|
$di->set(Service\PasswordRecoveryService::class);
|
||||||
$di->set(Service\ResendConfirmationService::class);
|
$di->set(Service\ResendConfirmationService::class);
|
||||||
$di->set(Service\ResetPasswordService::class);
|
$di->set(Service\ResetPasswordService::class);
|
||||||
@ -144,6 +149,18 @@ class Bootstrap implements BootstrapInterface
|
|||||||
$di->set(Search\RoleSearch::class);
|
$di->set(Search\RoleSearch::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attach an event to check if the password has expired
|
||||||
|
if (!is_null(Yii::$app->getModule('user')->maxPasswordAge)) {
|
||||||
|
Event::on(SecurityController::class, FormEvent::EVENT_AFTER_LOGIN, function (FormEvent $event) {
|
||||||
|
$user = $event->form->user;
|
||||||
|
if ($user->password_age >= Yii::$app->getModule('user')->maxPasswordAge) {
|
||||||
|
// Force password change
|
||||||
|
Yii::$app->session->setFlash('warning', Yii::t('usuario', 'Your password has expired, you must change it now'));
|
||||||
|
Yii::$app->response->redirect(['/user/settings/account'])->send();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if ($app instanceof WebApplication) {
|
if ($app instanceof WebApplication) {
|
||||||
// override Yii
|
// override Yii
|
||||||
$di->set(
|
$di->set(
|
||||||
|
|||||||
@ -18,6 +18,7 @@ use Da\User\Model\Profile;
|
|||||||
use Da\User\Model\User;
|
use Da\User\Model\User;
|
||||||
use Da\User\Query\UserQuery;
|
use Da\User\Query\UserQuery;
|
||||||
use Da\User\Search\UserSearch;
|
use Da\User\Search\UserSearch;
|
||||||
|
use Da\User\Service\PasswordExpireService;
|
||||||
use Da\User\Service\PasswordRecoveryService;
|
use Da\User\Service\PasswordRecoveryService;
|
||||||
use Da\User\Service\SwitchIdentityService;
|
use Da\User\Service\SwitchIdentityService;
|
||||||
use Da\User\Service\UserBlockService;
|
use Da\User\Service\UserBlockService;
|
||||||
@ -328,4 +329,20 @@ class AdminController extends Controller
|
|||||||
|
|
||||||
return $this->redirect(['index']);
|
return $this->redirect(['index']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Forces the user to change password at next login
|
||||||
|
* @param integer $id
|
||||||
|
*/
|
||||||
|
public function actionForcePasswordChange($id)
|
||||||
|
{
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $this->userQuery->where(['id' => $id])->one();
|
||||||
|
if ($this->make(PasswordExpireService::class, [$user])->run()) {
|
||||||
|
Yii::$app->session->setFlash("success", Yii::t('usuario', 'User will be required to change password at next login'));
|
||||||
|
} else {
|
||||||
|
Yii::$app->session->setFlash("danger", Yii::t('usuario', 'There was an error in saving user'));
|
||||||
|
}
|
||||||
|
$this->redirect(['index']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/User/Filter/PasswordAgeEnforceFilter.php
Normal file
39
src/User/Filter/PasswordAgeEnforceFilter.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the 2amigos/yii2-usuario project.
|
||||||
|
*
|
||||||
|
* (c) 2amigOS! <http://2amigos.us/>
|
||||||
|
* @author Lorenzo Milesi <maxxer@yetopen.it>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Da\User\Filter;
|
||||||
|
|
||||||
|
use Yii;
|
||||||
|
use yii\base\ActionFilter;
|
||||||
|
|
||||||
|
class PasswordAgeEnforceFilter extends ActionFilter
|
||||||
|
{
|
||||||
|
public function beforeAction($action)
|
||||||
|
{
|
||||||
|
$maxPasswordAge = Yii::$app->getModule('user')->maxPasswordAge;
|
||||||
|
// If feature is not set do nothing (or raise a configuration error?)
|
||||||
|
if (is_null($maxPasswordAge)) {
|
||||||
|
return parent::beforeAction($action);
|
||||||
|
}
|
||||||
|
if (Yii::$app->user->isGuest) {
|
||||||
|
// Not our business
|
||||||
|
return parent::beforeAction($action);
|
||||||
|
}
|
||||||
|
if (Yii::$app->user->identity->password_age >= $maxPasswordAge) {
|
||||||
|
// Force password change
|
||||||
|
Yii::$app->getSession()->setFlash('warning', Yii::t('usuario', 'Your password has expired, you must change it now'));
|
||||||
|
return Yii::$app->response->redirect(['/user/settings/account'])->send();
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::beforeAction($action);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the 2amigos/yii2-usuario project.
|
||||||
|
*
|
||||||
|
* (c) 2amigOS! <http://2amigos.us/>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Da\User\Migration;
|
||||||
|
|
||||||
|
use yii\db\Migration;
|
||||||
|
|
||||||
|
class m000000_000007_enable_password_expiration extends Migration
|
||||||
|
{
|
||||||
|
public function safeUp()
|
||||||
|
{
|
||||||
|
$this->addColumn('{{%user}}', 'password_changed_at', $this->integer()->null());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function safeDown()
|
||||||
|
{
|
||||||
|
$this->dropColumn('{{%user}}', 'password_changed_at');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -22,6 +22,7 @@ use yii\base\InvalidParamException;
|
|||||||
use yii\base\NotSupportedException;
|
use yii\base\NotSupportedException;
|
||||||
use yii\behaviors\TimestampBehavior;
|
use yii\behaviors\TimestampBehavior;
|
||||||
use yii\db\ActiveRecord;
|
use yii\db\ActiveRecord;
|
||||||
|
use yii\db\Expression;
|
||||||
use yii\helpers\ArrayHelper;
|
use yii\helpers\ArrayHelper;
|
||||||
use yii\web\Application;
|
use yii\web\Application;
|
||||||
use yii\web\IdentityInterface;
|
use yii\web\IdentityInterface;
|
||||||
@ -49,6 +50,8 @@ use yii\web\IdentityInterface;
|
|||||||
* @property int $created_at
|
* @property int $created_at
|
||||||
* @property int $updated_at
|
* @property int $updated_at
|
||||||
* @property int $last_login_at
|
* @property int $last_login_at
|
||||||
|
* @property int $password_changed_at
|
||||||
|
* @property int $password_age
|
||||||
*
|
*
|
||||||
* Defined relations:
|
* Defined relations:
|
||||||
* @property SocialNetworkAccount[] $socialNetworkAccounts
|
* @property SocialNetworkAccount[] $socialNetworkAccounts
|
||||||
@ -95,6 +98,7 @@ class User extends ActiveRecord implements IdentityInterface
|
|||||||
'password_hash',
|
'password_hash',
|
||||||
$security->generatePasswordHash($this->password, $this->getModule()->blowfishCost)
|
$security->generatePasswordHash($this->password, $this->getModule()->blowfishCost)
|
||||||
);
|
);
|
||||||
|
$this->password_changed_at = time();
|
||||||
}
|
}
|
||||||
|
|
||||||
return parent::beforeSave($insert);
|
return parent::beforeSave($insert);
|
||||||
@ -147,6 +151,8 @@ class User extends ActiveRecord implements IdentityInterface
|
|||||||
'created_at' => Yii::t('usuario', 'Registration time'),
|
'created_at' => Yii::t('usuario', 'Registration time'),
|
||||||
'confirmed_at' => Yii::t('usuario', 'Confirmation time'),
|
'confirmed_at' => Yii::t('usuario', 'Confirmation time'),
|
||||||
'last_login_at' => Yii::t('usuario', 'Last login'),
|
'last_login_at' => Yii::t('usuario', 'Last login'),
|
||||||
|
'password_changed_at' => Yii::t('usuario', 'Last password change'),
|
||||||
|
'password_age' => Yii::t('usuario', 'Password age'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,4 +334,17 @@ class User extends ActiveRecord implements IdentityInterface
|
|||||||
{
|
{
|
||||||
throw new NotSupportedException('Method "' . __CLASS__ . '::' . __METHOD__ . '" is not implemented.');
|
throw new NotSupportedException('Method "' . __CLASS__ . '::' . __METHOD__ . '" is not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns password age in days
|
||||||
|
* @return integer
|
||||||
|
*/
|
||||||
|
public function getPassword_age()
|
||||||
|
{
|
||||||
|
if (is_null($this->password_changed_at)) {
|
||||||
|
return $this->getModule()->maxPasswordAge;
|
||||||
|
}
|
||||||
|
$d = new \DateTime($this->password_changed_at);
|
||||||
|
return $d->diff(new \DateTime(), true)->format("%a");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -132,4 +132,8 @@ class Module extends BaseModule
|
|||||||
* @var string the session key name to impersonate users. Please, modify it for security reasons!
|
* @var string the session key name to impersonate users. Please, modify it for security reasons!
|
||||||
*/
|
*/
|
||||||
public $switchIdentitySessionKey = 'yuik_usuario';
|
public $switchIdentitySessionKey = 'yuik_usuario';
|
||||||
|
/**
|
||||||
|
* @var integer If != NULL sets a max password age in days
|
||||||
|
*/
|
||||||
|
public $maxPasswordAge = null;
|
||||||
}
|
}
|
||||||
|
|||||||
30
src/User/Service/PasswordExpireService.php
Normal file
30
src/User/Service/PasswordExpireService.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the 2amigos/yii2-usuario project.
|
||||||
|
*
|
||||||
|
* (c) 2amigOS! <http://2amigos.us/>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view
|
||||||
|
* the LICENSE file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Da\User\Service;
|
||||||
|
|
||||||
|
use Da\User\Contracts\ServiceInterface;
|
||||||
|
use Da\User\Model\User;
|
||||||
|
|
||||||
|
class PasswordExpireService implements ServiceInterface
|
||||||
|
{
|
||||||
|
protected $model;
|
||||||
|
|
||||||
|
public function __construct(User $model)
|
||||||
|
{
|
||||||
|
$this->model = $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run()
|
||||||
|
{
|
||||||
|
return $this->model->updateAttributes(['last_login_at' => time()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -92,6 +92,7 @@ $module = Yii::$app->getModule('user');
|
|||||||
'format' => 'raw',
|
'format' => 'raw',
|
||||||
'visible' => Yii::$app->getModule('user')->enableEmailConfirmation,
|
'visible' => Yii::$app->getModule('user')->enableEmailConfirmation,
|
||||||
],
|
],
|
||||||
|
'password_age',
|
||||||
[
|
[
|
||||||
'header' => Yii::t('usuario', 'Block status'),
|
'header' => Yii::t('usuario', 'Block status'),
|
||||||
'value' => function ($model) {
|
'value' => function ($model) {
|
||||||
@ -121,7 +122,7 @@ $module = Yii::$app->getModule('user');
|
|||||||
],
|
],
|
||||||
[
|
[
|
||||||
'class' => 'yii\grid\ActionColumn',
|
'class' => 'yii\grid\ActionColumn',
|
||||||
'template' => '{switch} {reset} {update} {delete}',
|
'template' => '{switch} {reset} {force-password-change} {update} {delete}',
|
||||||
'buttons' => [
|
'buttons' => [
|
||||||
'switch' => function ($url, $model) use ($module) {
|
'switch' => function ($url, $model) use ($module) {
|
||||||
if ($model->id != Yii::$app->user->id && $module->enableSwitchIdentities) {
|
if ($model->id != Yii::$app->user->id && $module->enableSwitchIdentities) {
|
||||||
@ -158,7 +159,24 @@ $module = Yii::$app->getModule('user');
|
|||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
},
|
||||||
|
'force-password-change' => function ($url, $model) use ($module) {
|
||||||
|
if (is_null($module->maxPasswordAge)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Html::a(
|
||||||
|
'<span class="glyphicon glyphicon-time"></span>',
|
||||||
|
['/user/admin/force-password-change', 'id' => $model->id],
|
||||||
|
[
|
||||||
|
'title' => Yii::t('usuario', 'Force password change at next login'),
|
||||||
|
'data-confirm' => Yii::t(
|
||||||
|
'usuario',
|
||||||
|
'Are you sure you wish the user to change their password at next login?'
|
||||||
|
),
|
||||||
|
'data-method' => 'POST',
|
||||||
|
]
|
||||||
|
);
|
||||||
|
},
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@ -73,6 +73,7 @@ CREATE TABLE `user` (
|
|||||||
`auth_tf_enabled` tinyint(1) DEFAULT '0',
|
`auth_tf_enabled` tinyint(1) DEFAULT '0',
|
||||||
`flags` int(11) NOT NULL DEFAULT '0',
|
`flags` int(11) NOT NULL DEFAULT '0',
|
||||||
`last_login_at` int(11) DEFAULT NULL,
|
`last_login_at` int(11) DEFAULT NULL,
|
||||||
|
`password_changed_at` int(11) DEFAULT NULL,
|
||||||
|
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `user_unique_email` (`email`),
|
UNIQUE KEY `user_unique_email` (`email`),
|
||||||
|
|||||||
Reference in New Issue
Block a user