diff --git a/README.md b/README.md index c40697f..357d35d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,40 @@ This package enforces 2FA for Laravel Nova. +## Upgrade from 0.0.7 to 1.0.0 + +Upgrade guide is available [Here](docs/upgrade_to_1.0.0.md'). + +## Flow + +### Activation + +- User gets recovery codes. + +![Recovery codes](docs/images/recovery-codes.png) + +- User activates 2FA on his device. + +![Activate 2FA](docs/images/register.png) + +### Verification + +- User verifies login with 2FA. + +![Enter 2FA](docs/images/enter-code.png) + +### Recovery + +- If user enters invalid code, recovery button is shown. + +![Enter 2FA](docs/images/invalid-code.png) + +- User enters recovery code. + +![Enter 2FA](docs/images/enter-recovery-code.png) + +- User is redirected to activation process. + ## Installation Install via composer @@ -60,13 +94,13 @@ return [ 'models' => [ /** - * Change this variable to your User model. + * Change this variable to path to user model. */ 'user' => 'App\User', ], 'tables' => [ /** - * Table in witch users are stored. + * Table in which users are stored. */ 'user' => 'users', ], @@ -86,6 +120,14 @@ return [ * Number of characters in each block in recovery code. */ 'chars_in_block' => 16, + + /** + * The following algorithms are currently supported: + * - PASSWORD_DEFAULT + * - PASSWORD_BCRYPT + * - PASSWORD_ARGON2I // available from php 7.2 + */ + 'hashing_algorithm' => PASSWORD_BCRYPT, ], ]; ``` diff --git a/config/lifeonscreen2fa.php b/config/lifeonscreen2fa.php index c1f3498..4de5719 100644 --- a/config/lifeonscreen2fa.php +++ b/config/lifeonscreen2fa.php @@ -34,5 +34,13 @@ * Number of characters in each block in recovery code. */ 'chars_in_block' => 16, + + /** + * The following algorithms are currently supported: + * - PASSWORD_DEFAULT + * - PASSWORD_BCRYPT + * - PASSWORD_ARGON2I // available from php 7.2 + */ + 'hashing_algorithm' => PASSWORD_BCRYPT, ], ]; \ No newline at end of file diff --git a/docs/images/enter-code.png b/docs/images/enter-code.png new file mode 100644 index 0000000..0699c49 Binary files /dev/null and b/docs/images/enter-code.png differ diff --git a/docs/images/enter-recovery-code.png b/docs/images/enter-recovery-code.png new file mode 100644 index 0000000..458478c Binary files /dev/null and b/docs/images/enter-recovery-code.png differ diff --git a/docs/images/invalid-code.png b/docs/images/invalid-code.png new file mode 100644 index 0000000..17ae5ca Binary files /dev/null and b/docs/images/invalid-code.png differ diff --git a/docs/images/recovery-codes.png b/docs/images/recovery-codes.png new file mode 100644 index 0000000..60b95b1 Binary files /dev/null and b/docs/images/recovery-codes.png differ diff --git a/docs/images/register.png b/docs/images/register.png new file mode 100644 index 0000000..35dc68d Binary files /dev/null and b/docs/images/register.png differ diff --git a/docs/upgrade_to_1.0.0.md b/docs/upgrade_to_1.0.0.md new file mode 100644 index 0000000..d6cd799 --- /dev/null +++ b/docs/upgrade_to_1.0.0.md @@ -0,0 +1,46 @@ +# Upgrade from 0.0.7 to 1.0.0 + +In version 1.0.0 recovery codes are hashed in database. + +Copy code bellow to migrations folder and run migrations: + +```php +recovery); + array_walk($recoveryCodes, function (&$value) { + $value = password_hash($value, config('lifeonscreen2fa.recovery_codes.hashing_algorithm')); + }); + $user2fa->recovery = json_encode($recoveryCodes); + $user2fa->save(); + } + DB::commit(); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + } +} +``` + diff --git a/src/Google2FAAuthenticator.php b/src/Google2FAAuthenticator.php index cf76486..652ad0d 100644 --- a/src/Google2FAAuthenticator.php +++ b/src/Google2FAAuthenticator.php @@ -2,7 +2,7 @@ namespace Lifeonscreen\Google2fa; -use App\Exceptions\ValidationException; +use Exception; use PragmaRX\Google2FALaravel\Support\Authenticator; /** @@ -21,13 +21,13 @@ protected function canPassWithoutCheckingOTP() /** * @return mixed - * @throws ValidationException + * @throws Exception */ protected function getGoogle2FASecretKey() { $secret = $this->getUser()->user2fa->{$this->config('otp_secret_column')}; if (is_null($secret) || empty($secret)) { - throw new ValidationException('Secret key cannot be empty.'); + throw new Exception('Secret key cannot be empty.'); } return $secret; diff --git a/src/Google2fa.php b/src/Google2fa.php index b3675d5..909ac46 100644 --- a/src/Google2fa.php +++ b/src/Google2fa.php @@ -86,13 +86,24 @@ public function register() } + private function isRecoveryValid($recover, $recoveryHashes) + { + foreach ($recoveryHashes as $recoveryHash) { + if (password_verify($recover, $recoveryHash)) { + return true; + } + } + + return false; + } + /** * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Http\Response|\Illuminate\View\View */ public function authenticate() { if ($recover = Request::get('recover')) { - if (in_array($recover, json_decode(auth()->user()->user2fa->recovery, true)) === false) { + if ($this->isRecoveryValid($recover, json_decode(auth()->user()->user2fa->recovery, true)) === false) { $data['error'] = 'Recovery key is invalid.'; return view('google2fa::authenticate', $data); @@ -107,12 +118,17 @@ public function authenticate() ->setChars(config('lifeonscreen2fa.recovery_codes.chars_in_block')) ->toArray(); + $recoveryHashes = $data['recovery']; + array_walk($recoveryHashes, function (&$value) { + $value = password_hash($value, config('lifeonscreen2fa.recovery_codes.hashing_algorithm')); + }); + User2fa::where('user_id', auth()->user()->id)->delete(); $user2fa = new User2fa(); $user2fa->user_id = auth()->user()->id; $user2fa->google2fa_secret = $secretKey; - $user2fa->recovery = json_encode($data['recovery']); + $user2fa->recovery = json_encode($recoveryHashes); $user2fa->save(); return response(view('google2fa::recovery', $data));