commit 768720a83a8448fad559f10589354a19d041aaf0 Author: Marcus Olsson Date: Fri Oct 16 12:15:46 2015 +0200 Initial commit. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cd8eb86 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b7ef35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor +composer.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f30edda --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: php +php: + - 5.5 + - 5.6 + - 7.0 + - nightly + +before_script: composer install diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..9134fe3 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) 2015 Marcus Olsson + +> Permission is hereby granted, free of charge, to any person obtaining a copy +> of this software and associated documentation files (the "Software"), to deal +> in the Software without restriction, including without limitation the rights +> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom the Software is +> furnished to do so, subject to the following conditions: +> +> The above copyright notice and this permission notice shall be included in +> all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +> THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..41e30ed --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# Zxcvbn for Laravel 5 + +[![Latest Version on Packagist][ico-version]][link-packagist] +[![Software License][ico-license]](LICENSE.md) +[![Build Status][ico-travis]][link-travis] + +A simple implementation of zxcvbn for Laravel 5. This package allows you to access "zxcvbn-related" data on a passphrase in the application, but also use zxcvbn as a standard validator. + +Uses [Zxcvbn-PHP](https://github.com/bjeavons/zxcvbn-php) by [@bjeavons](https://github.com/bjeavons), which in turn is inspired by [zxcvbn](https://github.com/dropbox/zxcvbn) by [@dropbox](https://github.com/dropbox). + +## Install + +Via Composer + +``` bash +$ composer require olssonm/l5-zxcvbn +``` + +Add the package to your providers array: + +``` php +'providers' => [ + Olssonm\Zxcvbn\ZxcvbnServiceProvider::class, +] +``` + +If you wish to have the ability to use `Zxcvbn` via dependency injection, or just have a quick way to access the class – add an alias to the facades: + +``` php +'aliases' => [ + 'Zxcvbn' => Olssonm\Zxcvbn\Facades\Zxcvbn::class +] +``` + +## Usage + +If you've added `Olssonm\Zxcvbn` as an alias, your can access Zxcvbn easily from anywhere in your application: + +### "In app" + +``` php +passwordStrength('password')); + + // array:6 [ + // "crack_time" => 5.0E-5 + // "calc_time" => 0.1857271194458 + // "password" => "password" + // "entropy" => 0.0 + // "match_sequence" => array:1 [] + // "score" => 0 + // ] + } +} +?> +``` + +Play around with different passwords and phrases, the results may surprise you. Check out [Zxcvbn-PHP](https://github.com/bjeavons/zxcvbn-php) for more uses and examples. + +### As a validator + +The package gives you two different validation rules that you may use; `zxcvbn_min` and `zxcvbn_dictionary`. + +#### zxcvbn_min + +`zxcvbn_min` allows you to set up a rule for minimum score that the value beeing tested should adhere to. + +***Syntax*** +`'input' => 'zxcvbn_min:min_value'` + +``` php + 'password']; + $validator = Validator::make($data, [ + 'password' => 'zxcvbn_min:3|required', + ], [ + 'password.zxcvbn_min' => 'Your password is not strong enough!' + ]); +``` + +In this example the password should at least have a "score" of three (3) to pass the validation. Of course, you should probably use the zxcvbn-library on the front-end too to allow the user to know this before posting the form... + +#### zxcvbn_dictionary + +This is a bit more interesting. `zxcvbn_dictionary` allows you to input both the users username and/or email, and their password. The validator checks that the password doesn't exist in the username, or that they are too similar. + +***Syntax*** +`'input' => 'xcvbn_dictionary:username,email'` + +``` php + 'user', + 'email' => 'trash@thedumpster.com' + ]; + $validator = Validator::make($password, [ + 'password' => 'zxcvbn_dictionary:' . $data['username'] . ',' . $data['email'] . '|required', + ]); + + dd($validator->passes()); + // true + + /** + * Example 2, fail + */ + $password = 'mycomplicatedphrase'; + $data = [ + 'username' => 'mycomplicatedphrase', + 'email' => 'mycomplicatedphrase@thedumpster.com' + ]; + $validator = Validator::make($password, [ + 'password' => 'zxcvbn_dictionary:' . $data['username'] . ',' . $data['email'] . '|required', + ]); + + dd($validator->passes()); + // false +``` + +## Testing + +``` bash +$ composer test +``` + +or + +``` bash +$ phpunit +``` + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. + +© 2015 [Marcus Olsson](https://marcusolsson.me). + +[ico-version]: https://img.shields.io/packagist/v/olssonm/l5-zxcvbn.svg?style=flat-square +[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square +[ico-travis]: https://img.shields.io/travis/olssonm/l5-zxcvbn/master.svg?style=flat-square +[link-packagist]: https://packagist.org/packages/olssonm/l5-zxcvbn +[link-travis]: https://travis-ci.org/olssonm/l5-zxcvbn diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b4ddfef --- /dev/null +++ b/composer.json @@ -0,0 +1,50 @@ +{ + "name": "olssonm/l5-zxcvbn", + "description": "Implementation of the zxcvbn project by @dropbox for Laravel 5. Uses zxcvbn-php by @bjeavons.", + "keywords": [ + "olssonm", + "zxcvbn", + "staple horse battery", + "passwords", + "validation", + "laravel" + ], + "homepage": "https://github.com/olssonm/l5-zxcvbn", + "license": "MIT", + "authors": [ + { + "name": "Marcus Olsson", + "email": "hello@marcusolsson.me", + "homepage": "https://marcusolsson.me" + } + ], + "require": { + "php" : ">=5.3.0", + "illuminate/support": "~5.1", + "bjeavons/zxcvbn-php": "0.1.4" + }, + "require-dev": { + "phpunit/phpunit" : "4.*", + "orchestra/testbench": "~3.0", + "scrutinizer/ocular": "~1.1" + }, + "autoload": { + "psr-4": { + "Olssonm\\Zxcvbn\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Olssonm\\Zxcvbn\\Test\\": "tests/" + } + }, + "scripts": { + "test": "phpunit" + }, + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "minimum-stability": "stable" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..3347b75 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests/ + + + diff --git a/src/Facades/Zxcvbn.php b/src/Facades/Zxcvbn.php new file mode 100644 index 0000000..83b5543 --- /dev/null +++ b/src/Facades/Zxcvbn.php @@ -0,0 +1,18 @@ +passwordStrength($value); + $target = 5; + + if(isset($parameters[0])) { + $target = $parameters[0]; + } + + return ($zxcvbn['score'] >= $target); + }, 'Your :input is not secure enough.'); + + Validator::replacer('zxcvbn_min', function($message, $attribute, $rule, $parameters) { + $message = str_replace(':input', $attribute, $message); + return $message; + }); + + /** + * Extend the Laravel Validator with the "zxcvbn_min" rule + */ + Validator::extend('zxcvbn_dictionary', function($attribute, $value, $parameters, $validator) { + $email = null; + $username = null; + + if(isset($parameters[0])) { + $email = $parameters[0]; + $username = $parameters[1]; + } + + $zxcvbn = new ZxcvbnPhp(); + $zxcvbn = $zxcvbn->passwordStrength($value, [$username, $email]); + + if(isset($zxcvbn['match_sequence'][0])) { + $dictionary = $zxcvbn['match_sequence'][0]; + if(isset($dictionary->dictionaryName)) { + return false; + } + } + + return true; + + }, 'Your :input is insecure. It either matches a commonly used password, or you have used a similar username/password combination.'); + + Validator::replacer('zxcvbn_dictionary', function($message, $attribute, $rule, $parameters) { + $message = str_replace(':input', $attribute, $message); + return $message; + }); + } + + /** + * Register any package services. + * + * @return void + */ + public function register() + { + $this->app['zxcvbn'] = $this->app->share(function ($app) { + return new ZxcvbnPhp($this->app['config']); + }); + } +} diff --git a/tests/ZxcvbnTest.php b/tests/ZxcvbnTest.php new file mode 100644 index 0000000..f539c8f --- /dev/null +++ b/tests/ZxcvbnTest.php @@ -0,0 +1,154 @@ + 'Olssonm\Zxcvbn\Facades\Zxcvbn' + ]; + } + + /** + * Just run som standard tests to see that Zxcvbn is up to snuff and working + * @test + */ + public function test_zxcvbn_basics() + { + $testVar1 = Zxcvbn::passwordStrength('test'); + + // Check keys + $this->assertArrayHasKey('score', $testVar1); + $this->assertArrayHasKey('match_sequence', $testVar1); + $this->assertArrayHasKey('entropy', $testVar1); + $this->assertArrayHasKey('password', $testVar1); + $this->assertArrayHasKey('calc_time', $testVar1); + $this->assertArrayHasKey('crack_time', $testVar1); + + // Check score-value + $this->assertEquals(0, $testVar1['score']); + + // Run some more tests + $testVar2 = Zxcvbn::passwordStrength('dadaurka'); + $testVar3 = Zxcvbn::passwordStrength('staple horse battery'); + $testVar4 = Zxcvbn::passwordStrength('7E6k9axB*gwGHa&aZTohmD9Wr&NVs[b4'); //<-- 32 + + // Check score-value + $this->assertEquals(1, $testVar2['score']); + $this->assertEquals(4, $testVar3['score']); + $this->assertEquals(4, $testVar4['score']); + } + + /** @test */ + public function test_password_strength() + { + // Standard tests + $this->assertEquals(true, $this->validate_without_message_min('test', 0)); + $this->assertEquals(false, $this->validate_without_message_min('test', 4)); + + $this->assertEquals(true, $this->validate_without_message_min('staple horse battery', 3)); + $this->assertEquals(true, $this->validate_without_message_min('staple horse battery', 4)); + $this->assertEquals(false, $this->validate_without_message_min('staple horse battery', 5)); + } + + /** @test */ + public function test_password_strength_with_message() + { + // Standard message + $this->assertEquals('Your password is not secure enough.', $this->validate_with_message_min('staple horse battery', 5, null)); + $this->assertEquals('Just a message', $this->validate_with_message_min('test', 4, 'Just a message')); + } + + /** @test */ + public function test_password_dictionary() + { + // Standard tests + $this->assertEquals(false, $this->validate_without_message_dictionary('password', 'test@test.com', 'test')); + $this->assertEquals(false, $this->validate_without_message_dictionary('test', 'test@test.com', 'test')); + $this->assertEquals(false, $this->validate_without_message_dictionary('721ahsa!', '721ahsa@test.com', '721ahsa')); + + $this->assertEquals(true, $this->validate_without_message_dictionary('721ahsa!', 'dadaurka@test.com', 'dadaurka')); + $this->assertEquals(true, $this->validate_without_message_dictionary('asd912j!', 'myemail@test.com', 'username')); + $this->assertEquals(true, $this->validate_without_message_dictionary('asd912j!', 'trash@thedumpster.com', 'username')); + + $this->assertEquals(true, $this->validate_without_message_dictionary('asd912j!', null, 'username')); + $this->assertEquals(true, $this->validate_without_message_dictionary('asd912j!', null, null)); + } + + /** @test */ + public function test_password_dictionary_with_message() + { + // Standard message + $this->assertEquals('Your password is insecure. It either matches a commonly used password, or you have used a similar username/password combination.', $this->validate_with_message_dictionary('password', 'test@test.com', 'test', null)); + $this->assertEquals('Just a message', $this->validate_with_message_dictionary('test', 'test@test.com', 'test', 'Just a message')); + } + + private function validate_without_message_min($password, $min) + { + $data = ['password' => $password]; + $validator = Validator::make($data, [ + 'password' => 'zxcvbn_min:' . $min . '|required', + ]); + + return $validator->passes(); + } + + private function validate_with_message_min($password, $min, $message) + { + $data = ['password' => $password]; + $validator = Validator::make($data, [ + 'password' => 'zxcvbn_min:' . $min . '|required', + ], [ + 'password.zxcvbn_min' => $message + ]); + + $errors = $validator->errors(); + return $errors->first('password'); + } + + private function validate_without_message_dictionary($password, $email, $username) + { + $data = ['password' => $password]; + $validator = Validator::make($data, [ + 'password' => 'zxcvbn_dictionary:' . $username . ',' . $email . '|required', + ]); + + return $validator->passes(); + } + + private function validate_with_message_dictionary($password, $email, $username, $message) + { + $data = ['password' => $password]; + $validator = Validator::make($data, [ + 'password' => 'zxcvbn_dictionary:' . $username . ',' . $email . '|required', + ], [ + 'password.zxcvbn_dictionary' => $message + ]); + + $errors = $validator->errors(); + return $errors->first('password'); + } +}