├── src ├── Observers │ └── UserObserver.php ├── Database │ ├── PasswordHistory.php │ ├── migrations │ │ └── 2018_04_08_033256_create_password_histories_table.php │ └── PasswordHistoryRepo.php ├── Facades │ └── PasswordHistoryManager.php ├── config │ └── password_history.php ├── Rules │ └── NotBeInPasswordHistory.php ├── PasswordHistoryServiceProvider.php └── PasswordHistory.php ├── LICENSE ├── composer.json └── README.md /src/Observers/UserObserver.php: -------------------------------------------------------------------------------- 1 | table = config('password_history.table_name'); 16 | parent::__construct($attributes); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/Facades/PasswordHistoryManager.php: -------------------------------------------------------------------------------- 1 | increments('id'); 13 | $table->integer('user_id')->unsigned(); 14 | $table->string('guard', 20); 15 | $table->string('password', 80); 16 | $table->timestamps(); 17 | }); 18 | } 19 | 20 | public function down() 21 | { 22 | Schema::dropIfExists(config('password_history.table_name')); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/config/password_history.php: -------------------------------------------------------------------------------- 1 | 'password_histories', 8 | 9 | /** 10 | * The number of most recent previous passwords to check against when changing/resetting a password 11 | * false is off which doesn't log password changes or check against them 12 | */ 13 | 'check_depth' => env('PASSWORD_HISTORY_DEPTH', 3), 14 | 15 | /** 16 | * The models to be observed on the "saved" event 17 | */ 18 | 'models' => [ 19 | /** 20 | 'App\User' => [ 21 | 'password_column' => 'password', 22 | 'guard' => 'user', 23 | ], 24 | \App\Admin::class => [ 25 | 'password_column' => 'password', 26 | 'guard' => 'admin', 27 | ], 28 | */ 29 | ], 30 | ]; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Iman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Rules/NotBeInPasswordHistory.php: -------------------------------------------------------------------------------- 1 | user = $user; 17 | $this->depth = $depth; 18 | } 19 | 20 | public static function ofUser($user, $depth = null) 21 | { 22 | return new static($user, $depth); 23 | } 24 | 25 | /** 26 | * Determine if the validation rule passes. 27 | * 28 | * @param string $attribute 29 | * @param mixed $value 30 | * 31 | * @return bool 32 | */ 33 | public function passes($attribute, $value) 34 | { 35 | $depth = $this->depth ?: config('password_history.check_depth'); 36 | 37 | return ! PasswordHistoryManager::isInHistoryOfUser($value, $this->user, $depth); 38 | } 39 | 40 | /** 41 | * Get the validation error message. 42 | * 43 | * @return string 44 | */ 45 | public function message() 46 | { 47 | return __('auth.password_used'); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Database/PasswordHistoryRepo.php: -------------------------------------------------------------------------------- 1 | where('user_id', $userId); 20 | 21 | if ($guard) { 22 | $q->where('guard', $guard); 23 | } 24 | 25 | if (! is_null($offset)) { 26 | $q->offset($offset); 27 | } 28 | 29 | if (! is_null($depth)) { 30 | $q->take($depth); 31 | } 32 | 33 | return $q->latest('id')->get(); 34 | } 35 | 36 | public static function getCurrentPassword($id, $guard) 37 | { 38 | return self::fetch($id, 1, $guard, 0)->first(); 39 | } 40 | 41 | public static function logNewPassword($password, $user_id, $guard = '') 42 | { 43 | return PasswordHistory::query()->create(get_defined_vars()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/PasswordHistoryServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfig(); 15 | } 16 | 17 | public function boot() 18 | { 19 | if ($this->app->runningInConsole()) { 20 | $this->publishes([__DIR__ .'/config/password_history.php' => config_path('password_history.php')]); 21 | $this->setMigrationFolder(); 22 | } 23 | 24 | $this->listenForModelChanges(); 25 | } 26 | 27 | private function listenForModelChanges() 28 | { 29 | $userModels = array_keys(config('password_history.models')); 30 | 31 | foreach ($userModels as $userModel) { 32 | $userModel::observe(UserObserver::class); 33 | } 34 | } 35 | 36 | private function setMigrationFolder() 37 | { 38 | $this->loadMigrationsFrom(__DIR__ . '/Database/migrations'); 39 | } 40 | 41 | private function mergeConfig() 42 | { 43 | $configFile = __DIR__.'/config/password_history.php'; 44 | 45 | /*if($this->app->runningUnitTests()) { 46 | $configFile = __DIR__.'/../tests/Requirements/config/password_history.php'; 47 | }*/ 48 | 49 | $this->mergeConfigFrom($configFile, 'password_history'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imanghafoori/laravel-password-history", 3 | "description": "A package to keep a history of all password changes of users", 4 | "keywords": [ 5 | "laravel", 6 | "laravel-authentication", 7 | "laravel-login", 8 | "laravel-security", 9 | "laravel-password", 10 | "laravel-package", 11 | "PHP" 12 | ], 13 | "license": "MIT", 14 | "homepage": "https://github.com/imanghafoori1/laravel-password-history", 15 | "authors": [ 16 | { 17 | "name": "Iman Ghafoori", 18 | "email": "imanghafoori1@gmail.com" 19 | } 20 | ], 21 | "require": { 22 | "php": "^7.1.3|7.2.*|7.3.*|7.4.*|8.*", 23 | "laravel/framework": "~5.1|6.*|7.*|8.*|9.*|10.*|11.*|12.*", 24 | "imanghafoori/laravel-nullable": "^1.2", 25 | "imanghafoori/laravel-smart-facades": "^1.0" 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Imanghafoori\\PasswordHistoryTests\\" : "tests" 30 | } 31 | }, 32 | "require-dev": { 33 | "orchestra/testbench": "~3.0", 34 | "mockery/mockery": "*" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "Imanghafoori\\PasswordHistory\\" : "src" 39 | } 40 | }, 41 | "suggest": { 42 | "imanghafoori/laravel-heyman": "It allows to write expressive code to authorize, validate and authenticate.", 43 | "imanghafoori/laravel-decorator": "Allows you to easily apply the decorator pattern.", 44 | "imanghafoori/laravel-terminator": "Gives you opportunity to refactor your controllers.", 45 | "imanghafoori/laravel-anypass": " Allows you login with any password in local environment.", 46 | "imanghafoori/laravel-masterpass": "You can easily set a master password without code change." 47 | }, 48 | "extra": { 49 | "laravel": { 50 | "providers": [ 51 | "Imanghafoori\\PasswordHistory\\PasswordHistoryServiceProvider" 52 | ] 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/PasswordHistory.php: -------------------------------------------------------------------------------- 1 | getPasswordCol($user); 13 | 14 | if ($user->$passwordCol && $user->isDirty($passwordCol)) { 15 | $this->logPasswordForUser($user->$passwordCol, $user); 16 | } 17 | } 18 | 19 | public function getGuard($user) 20 | { 21 | $models = config('password_history.models'); 22 | 23 | return $models[get_class($user)]['guard'] ?? strtolower(class_basename($user)); 24 | } 25 | 26 | private function getPasswordCol($user) 27 | { 28 | $models = config('password_history.models'); 29 | 30 | return $models[get_class($user)]['password_column'] ?? 'password'; 31 | } 32 | 33 | public function isInHistoryOfUser($password, $user, $depth = null) 34 | { 35 | return $this->isInHistory($password, $user->getKey(), $depth, $this->getGuard($user)); 36 | } 37 | 38 | public function isInHistory($password, $userId, $depth = null, $guard = '') 39 | { 40 | $depth = $depth ?: config('password_history.check_depth'); 41 | $histories = PasswordHistoryRepo::getAllPasswords($userId, $depth, $guard); 42 | 43 | foreach ($histories as $history) { 44 | if (Hash::check($password, $history->password)) { 45 | return true; 46 | } 47 | } 48 | 49 | return false; 50 | } 51 | 52 | public function latestChangeDate($user) 53 | { 54 | $password = $this->getOfDepth($user, 1)->first(); 55 | 56 | return nullable($password ? $password->created_at : null); 57 | } 58 | 59 | public function getCurrentPassword($user) 60 | { 61 | $password = $this->getOfDepth($user, 1)->first(); 62 | $passwordCol = $this->getPasswordCol($user); 63 | 64 | return nullable($password->$passwordCol ?? null); 65 | } 66 | 67 | public function passwordChangesCount($user) 68 | { 69 | // When there is no password at all, we return 0 (not -1) 70 | $count = $this->getOfDepth($user, null)->count(); 71 | 72 | return $count ? $count - 1 : 0; 73 | } 74 | 75 | public function logPasswordForUser($passwordHash, $user) 76 | { 77 | return PasswordHistoryRepo::logNewPassword($passwordHash, $user->getKey(), $this->getGuard($user)); 78 | } 79 | 80 | private function getOfDepth($user, $depth) 81 | { 82 | return PasswordHistoryRepo::fetch($user->getKey(), $depth, $this->getGuard($user), 0); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Password History 2 | Keep a password history of your users to prevent them from reusing the same password, for security reasons like what google does. 3 | 4 | [![StyleCI](https://github.styleci.io/repos/139709518/shield?branch=master)](https://github.styleci.io/repos/139709518) 5 | [![Latest Stable Version](https://poser.pugx.org/imanghafoori/laravel-password-history/v/stable)](https://packagist.org/packages/imanghafoori/laravel-password-history) 6 | [![Daily Downloads](https://poser.pugx.org/imanghafoori/laravel-password-history/d/daily)](https://packagist.org/packages/imanghafoori/laravel-password-history) 7 | [![Total Downloads](https://poser.pugx.org/imanghafoori/laravel-password-history/downloads)](https://packagist.org/packages/imanghafoori/laravel-password-history) 8 | [![Software License](https://img.shields.io/badge/license-MIT-blue.svg?style=round-square)](LICENSE.md) 9 | [![Imports](https://github.com/imanghafoori1/laravel-password-history/actions/workflows/check_imports.yml/badge.svg?branch=master)](https://github.com/imanghafoori1/laravel-password-history/actions/workflows/check_imports.yml) 10 | 11 | ## Installation: 12 | ``` 13 | composer require imanghafoori/laravel-password-history 14 | ``` 15 | 16 | To publish the config file and migrate the database: 17 | ``` 18 | php artisan vendor:publish 19 | ``` 20 | ``` 21 | php artisan migrate 22 | ``` 23 | 24 | Visit the `config/password_history.php` file to see all the possibilities. 25 | 26 | ## Usage: 27 | 28 | This package will observe the `saved` event of the models (which are mentioned in the config file) and records the password hashes automatically. 29 | ```php 30 | password = $passHash; 39 | $user->save(); // after saving the model, the password change will be recorded, automatically 40 | ``` 41 | 42 | We suggest to use `saveOrFail` to do all the queries in a transaction 43 | ``` 44 | $user->saveOrFail(); 45 | ``` 46 | 47 | Be careful that changing the model like below does not fire any model event hence to password change would be recorded behind the scenes. 48 | 49 | ```php 50 | update($data); 54 | ``` 55 | 56 | ### Validation Rules 57 | 58 | And there is a validation rule for you to check the entire password history agaist the new password in laravel validation rules. 59 | ```php 60 | [ 67 | 'required', 68 | 'confirmed', 69 | NotBeInPasswordHistory::ofUser($this->user), 70 | ] 71 | // ... 72 | ]; 73 | 74 | $this->validate(...); 75 | ``` 76 | 77 | Again you may want to take a quick look at the source code to see what is going on there. 78 | 79 | 80 | ## QA 81 | 82 | - I have a `users` table and an `admins` table (User model and Admin model), can I also track password changes for admins? 83 | ``` 84 | Yeah, the package supports it, visit the config file. 85 | ``` 86 | 87 | -------------------- 88 | 89 | ### :raising_hand: Contributing 90 | If you find an issue or have a better way to do something, feel free to open an issue or a pull request. 91 | 92 | ### :exclamation: Security 93 | If you discover any security-related issues, please use the `security tab` instead of using the issue tracker. 94 | 95 | 96 | ### :star: Your Stars Make Us Do More :star: 97 | As always if you found this package useful and you want to encourage us to maintain and work on it. Just press the star button to declare your willingness. 98 | 99 | 100 | 101 | ## More from the author: 102 | 103 | 104 | ### Laravel middlewarize 105 | 106 | :gem: You can put middleware on any method calls. 107 | 108 | - https://github.com/imanghafoori1/laravel-middlewarize 109 | 110 | ------------- 111 | 112 | ### Laravel HeyMan 113 | 114 | :gem: It allows us to write expressive code to authorize, validate and authenticate. 115 | 116 | - https://github.com/imanghafoori1/laravel-heyman 117 | 118 | 119 | -------------- 120 | 121 | ### Laravel Terminator 122 | 123 | 124 | :gem: A minimal yet powerful package to give you the opportunity to refactor your controllers. 125 | 126 | - https://github.com/imanghafoori1/laravel-terminator 127 | 128 | 129 | ------------ 130 | 131 | ### Laravel AnyPass 132 | 133 | :gem: It allows you to login with any password in the local environment only. 134 | 135 | - https://github.com/imanghafoori1/laravel-anypass 136 | 137 | ------------ 138 | 139 | 140 |

141 | 142 | A man will never fail, unless he stops trying. 143 | 144 | "Albert Einstein" 145 | 146 |

147 | 148 | --------------------------------------------------------------------------------