Merge pull request #136 from maxxer/password_expiration

Password expiration
This commit is contained in:
Antonio Ramirez
2018-02-07 22:51:39 +01:00
committed by GitHub
11 changed files with 194 additions and 2 deletions

View File

@ -6,6 +6,7 @@
- Bug #133: Fix user search returning no results in admin page (phiurs)
- Bug #125: Fix validation in non-ajax requests (faenir)
- Bug #122: Fix wrong email message for email address change (liviuk2)
- Bug #102: Implemented password expiration feature (maxxer)
## 1.1.1 - November 27, 2017
- Bug #115: Convert client_id to string because pgsql fail with type convertion (Dezinger)

View File

@ -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
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`)
If `true` users will be able to remove their own accounts.

View File

@ -25,6 +25,10 @@ use yii\console\Application as ConsoleApplication;
use yii\i18n\PhpMessageSource;
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,
* builds class map, and does the other setup actions participating in the application bootstrap process.
@ -91,6 +95,7 @@ class Bootstrap implements BootstrapInterface
// services
$di->set(Service\AccountConfirmationService::class);
$di->set(Service\EmailChangeService::class);
$di->set(Service\PasswordExpireService::class);
$di->set(Service\PasswordRecoveryService::class);
$di->set(Service\ResendConfirmationService::class);
$di->set(Service\ResetPasswordService::class);
@ -144,6 +149,18 @@ class Bootstrap implements BootstrapInterface
$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) {
// override Yii
$di->set(

View File

@ -18,6 +18,7 @@ use Da\User\Model\Profile;
use Da\User\Model\User;
use Da\User\Query\UserQuery;
use Da\User\Search\UserSearch;
use Da\User\Service\PasswordExpireService;
use Da\User\Service\PasswordRecoveryService;
use Da\User\Service\SwitchIdentityService;
use Da\User\Service\UserBlockService;
@ -328,4 +329,20 @@ class AdminController extends Controller
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']);
}
}

View 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);
}
}

View File

@ -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');
}
}

View File

@ -22,6 +22,7 @@ use yii\base\InvalidParamException;
use yii\base\NotSupportedException;
use yii\behaviors\TimestampBehavior;
use yii\db\ActiveRecord;
use yii\db\Expression;
use yii\helpers\ArrayHelper;
use yii\web\Application;
use yii\web\IdentityInterface;
@ -49,6 +50,8 @@ use yii\web\IdentityInterface;
* @property int $created_at
* @property int $updated_at
* @property int $last_login_at
* @property int $password_changed_at
* @property int $password_age
*
* Defined relations:
* @property SocialNetworkAccount[] $socialNetworkAccounts
@ -95,6 +98,7 @@ class User extends ActiveRecord implements IdentityInterface
'password_hash',
$security->generatePasswordHash($this->password, $this->getModule()->blowfishCost)
);
$this->password_changed_at = time();
}
return parent::beforeSave($insert);
@ -147,6 +151,8 @@ class User extends ActiveRecord implements IdentityInterface
'created_at' => Yii::t('usuario', 'Registration time'),
'confirmed_at' => Yii::t('usuario', 'Confirmation time'),
'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.');
}
/**
* 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");
}
}

View File

@ -132,4 +132,8 @@ class Module extends BaseModule
* @var string the session key name to impersonate users. Please, modify it for security reasons!
*/
public $switchIdentitySessionKey = 'yuik_usuario';
/**
* @var integer If != NULL sets a max password age in days
*/
public $maxPasswordAge = null;
}

View 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()]);
}
}

View File

@ -92,6 +92,7 @@ $module = Yii::$app->getModule('user');
'format' => 'raw',
'visible' => Yii::$app->getModule('user')->enableEmailConfirmation,
],
'password_age',
[
'header' => Yii::t('usuario', 'Block status'),
'value' => function ($model) {
@ -121,7 +122,7 @@ $module = Yii::$app->getModule('user');
],
[
'class' => 'yii\grid\ActionColumn',
'template' => '{switch} {reset} {update} {delete}',
'template' => '{switch} {reset} {force-password-change} {update} {delete}',
'buttons' => [
'switch' => function ($url, $model) use ($module) {
if ($model->id != Yii::$app->user->id && $module->enableSwitchIdentities) {
@ -158,7 +159,24 @@ $module = Yii::$app->getModule('user');
}
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',
]
);
},
]
],
],

View File

@ -73,6 +73,7 @@ CREATE TABLE `user` (
`auth_tf_enabled` tinyint(1) DEFAULT '0',
`flags` int(11) NOT NULL DEFAULT '0',
`last_login_at` int(11) DEFAULT NULL,
`password_changed_at` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `user_unique_email` (`email`),