Skip to content
This repository has been archived by the owner on Feb 17, 2022. It is now read-only.

Commit

Permalink
Merge pull request #65 from DarkGhostHunter/master
Browse files Browse the repository at this point in the history
Version 4.0
  • Loading branch information
DarkGhostHunter committed Sep 5, 2021
2 parents c1073d0 + bc0ee29 commit 854560c
Show file tree
Hide file tree
Showing 47 changed files with 1,210 additions and 1,510 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Expand Up @@ -9,3 +9,4 @@
/.scrutinizer.yml export-ignore
/tests export-ignore
/.editorconfig export-ignore
/laraguardlogo.png export-ignore
8 changes: 8 additions & 0 deletions .github/dependabot.yml
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: composer
directory: "/"
schedule:
interval: daily
time: "09:00"
open-pull-requests-limit: 10
8 changes: 4 additions & 4 deletions .github/workflows/php.yml
Expand Up @@ -11,12 +11,12 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [8.0, 7.4]
laravel: [8.*]
php: [8.0]
laravel: [^8.39]
dependency-version: [prefer-lowest, prefer-stable]
include:
- laravel: 8.*
testbench: 6.*
- laravel: ^8.39
testbench: ^6.20.1

name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.dependency-version }}

Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Expand Up @@ -2,4 +2,7 @@ build
composer.lock
docs
vendor
coverage
coverage
.idea
/.phpunit.result.cache
/phpunit.xml.dist.bak
301 changes: 131 additions & 170 deletions README.md

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions UPGRADE.md
@@ -0,0 +1,18 @@
# Upgrading

## Upgrade from 3.0

If you're upgrading from Laraguard 3.0, you will need to migrate.

Laraguard 4.0 encrypts the Shared Secret and Recovery Codes. This adds an extra layer of protection in case the database records are leaked to the wild, as recommended by the [RFC 6238](https://datatracker.ietf.org/doc/html/rfc6238).

To upgrade, ensure you have installed `doctrine/dbal` so the migration can run, as it needs to change a column type.

composer require doctrine/dbal

Then, publish the upgrading migration and run it:

php artisan vendor:publish --provider="DarkGhostHunter\Laraguard\LaraguardServiceProvider" --tag="upgrade"
php artisan migrate

The migration will automatically encrypt all shared secrets, while also reverting the decryption on rolling back migrations.
19 changes: 11 additions & 8 deletions composer.json
Expand Up @@ -21,19 +21,22 @@
}
],
"require": {
"php": "^7.4||^8.0",
"php": "^8.0",
"ext-json": "*",
"bacon/bacon-qr-code": "^2.0",
"paragonie/constant_time_encoding": "^2.4",
"illuminate/support": "^8.0",
"illuminate/http": "^8.20",
"illuminate/auth": "^8.0"
"illuminate/config": "^8.39",
"illuminate/validation": "^8.39",
"illuminate/database": "^8.39",
"illuminate/support": "^8.39",
"illuminate/http": "^8.39",
"illuminate/auth": "^8.39"
},
"require-dev": {
"orchestra/testbench": "^6.0",
"orchestra/canvas": "^6.0",
"mockery/mockery":"^1.4",
"phpunit/phpunit": "^9.3"
"doctrine/dbal": "^3.1",
"mockery/mockery": "^1.4",
"orchestra/testbench": "6.*",
"phpunit/phpunit": "^9.5.9"
},
"autoload": {
"psr-4": {
Expand Down
28 changes: 1 addition & 27 deletions config/laraguard.php
@@ -1,20 +1,6 @@
<?php

return [

/*
|--------------------------------------------------------------------------
| Listener hook
|--------------------------------------------------------------------------
|
| If a Listener class is present, Laraguard will hook into the Attempting
| and Validated events and check if it needs Two Factor Authentication.
| Set this to false or null to use your own 2FA logic without events.
|
*/

'listener' => \DarkGhostHunter\Laraguard\Listeners\EnforceTwoFactorAuth::class,

/*
|--------------------------------------------------------------------------
| TwoFactorAuthentication Model
Expand All @@ -28,19 +14,6 @@

'model' => \DarkGhostHunter\Laraguard\Eloquent\TwoFactorAuthentication::class,

/*
|--------------------------------------------------------------------------
| Input name
|--------------------------------------------------------------------------
|
| When using the Listener, it will automatically check the Request for the
| input name containing the Two Factor Code. A safe default is set here,
| but you can override the value if it collides with other form input.
|
*/

'input' => '2fa_code',

/*
|--------------------------------------------------------------------------
| Cache Store
Expand Down Expand Up @@ -86,6 +59,7 @@
*/

'safe_devices' => [
'cookie' => '2fa_remember',
'enabled' => false,
'max_devices' => 3,
'expiration_days' => 14,
Expand Down
80 changes: 37 additions & 43 deletions database/factories/TwoFactorAuthenticationFactory.php
Expand Up @@ -2,12 +2,12 @@

namespace Database\Factories\DarkGhostHunter\Laraguard\Eloquent;

use Faker\Generator as Faker;
use DarkGhostHunter\Laraguard\Eloquent\TwoFactorAuthentication;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Collection;
use DarkGhostHunter\Laraguard\Eloquent\TwoFactorAuthentication;

class TwoFactorAuthenticationFactory extends Factory {
class TwoFactorAuthenticationFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
Expand All @@ -20,7 +20,7 @@ class TwoFactorAuthenticationFactory extends Factory {
*
* @return array
*/
public function definition()
public function definition(): array
{
$config = config('laraguard');

Expand All @@ -34,7 +34,7 @@ public function definition()

if ($enabled) {
$array['recovery_codes'] = TwoFactorAuthentication::generateRecoveryCodes($amount, $length);
$array['recovery_codes_generated_at'] = $this->faker->dateTimeBetween('-1 years');
$array['recovery_codes_generated_at'] = $this->faker->dateTimeBetween('-1 year');
}

return $array;
Expand All @@ -43,60 +43,54 @@ public function definition()
/**
* Returns a model with unused recovery codes.
*
* @return TwoFactorAuthenticationFactory
* @return \Database\Factories\DarkGhostHunter\Laraguard\Eloquent\TwoFactorAuthenticationFactory
*/
public function withRecovery()
public function withRecovery(): static
{
return $this->state(function(array $attributes) {
[$enabled, $amount, $length] = array_values(config('laraguard.recovery'));
[$enabled, $amount, $length] = array_values(config('laraguard.recovery'));

return [
'recovery_codes' => TwoFactorAuthentication::generateRecoveryCodes($amount, $length),
'recovery_codes_generated_at' => $this->faker->dateTimeBetween('-1 years'),
];
});
return $this->state([
'recovery_codes' => TwoFactorAuthentication::generateRecoveryCodes($amount, $length),
'recovery_codes_generated_at' => $this->faker->dateTimeBetween('-1 years'),
]);
}

/**
* Returns an authentication with a list of safe devices.
*
* @return TwoFactorAuthenticationFactory
* @return \Database\Factories\DarkGhostHunter\Laraguard\Eloquent\TwoFactorAuthenticationFactory
*/
public function withSafeDevices()
public function withSafeDevices(): static
{
return $this->state(function (array $attributes) {
$max = config('laraguard.safe_devices.max_devices');

return [
'safe_devices' => Collection::times($max, function ($step) use ($max) {

$expiration_days = config('laraguard.safe_devices.expiration_days');

$added_at = $max !== $step
? now()
: $this->faker->dateTimeBetween(now()->subDays($expiration_days * 2), now()->subDays($expiration_days));

return [
'2fa_remember' => TwoFactorAuthentication::generateDefaultTwoFactorRemember(),
'ip' => $this->faker->ipv4,
'added_at' => $added_at,
];
}),
];
});
$max = config('laraguard.safe_devices.max_devices');

return $this->state([
'safe_devices' => Collection::times($max, function ($step) use ($max) {
$expiration_days = config('laraguard.safe_devices.expiration_days');

$added_at = $max !== $step
? now()
: $this->faker->dateTimeBetween(now()->subDays($expiration_days * 2),
now()->subDays($expiration_days));

return [
'2fa_remember' => TwoFactorAuthentication::generateDefaultTwoFactorRemember(),
'ip' => $this->faker->ipv4,
'added_at' => $added_at,
];
}),
]);
}

/**
* Returns an enabled authentication.
*
* @return TwoFactorAuthenticationFactory
* @return \Database\Factories\DarkGhostHunter\Laraguard\Eloquent\TwoFactorAuthenticationFactory
*/
public function enabled()
public function enabled(): static
{
return $this->state(function (array $attributes) {
return [
'enabled_at' => null
];
});
return $this->state([
'enabled_at' => null,
]);
}
}
Expand Up @@ -11,19 +11,19 @@ class CreateTwoFactorAuthenticationsTable extends Migration
*
* @return void
*/
public function up()
public function up(): void
{
Schema::create('two_factor_authentications', function (Blueprint $table) {
$table->bigIncrements('id');
$table->id();
$table->morphs('authenticatable', '2fa_auth_type_auth_id_index');
$table->string('shared_secret');
$table->text('shared_secret');
$table->timestampTz('enabled_at')->nullable();
$table->string('label');
$table->unsignedTinyInteger('digits')->default(6);
$table->unsignedTinyInteger('seconds')->default(30);
$table->unsignedTinyInteger('window')->default(0);
$table->string('algorithm', 16)->default('sha1');
$table->json('recovery_codes')->nullable();
$table->text('recovery_codes')->nullable();
$table->timestampTz('recovery_codes_generated_at')->nullable();
$table->json('safe_devices')->nullable();
$table->timestampsTz();
Expand All @@ -35,7 +35,7 @@ public function up()
*
* @return void
*/
public function down()
public function down(): void
{
Schema::dropIfExists('two_factor_authentications');
}
Expand Down
@@ -0,0 +1,85 @@
<?php

use Composer\InstalledVersions;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

class UpgradeTwoFactorAuthenticationsTable extends Migration
{
/**
* Creates a new Migration instance.
*
* @return void
*/
public function __construct()
{
if (! InstalledVersions::isInstalled('doctrine/dbal')) {
throw new OutOfBoundsException("Install the doctrine/dbal package to upgrade or downgrade.");
}
}

/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('two_factor_authentications', static function (Blueprint $table): void {
$table->text('shared_secret')->change();
$table->text('recovery_codes')->nullable()->change();
});

// We need to encrypt all shared secrets so these can be used with Laraguard v4.0.
$this->chunkRows(true);
}

/**
* Returns a chunk of authentications to encrypt/decrypt them.
*
* @param bool $encrypt
*
* @return void
*/
protected function chunkRows(bool $encrypt): void
{
$call = $encrypt ? 'encryptString' : 'decryptString';
$encrypter = Crypt::getFacadeRoot();
$query = DB::table('two_factor_authentications');

$query->clone()->select('id', 'shared_secret', 'recovery_codes')
->chunkById(
1000,
static function (Collection $chunk) use ($encrypter, $query, $call): void {
DB::beginTransaction();
foreach ($chunk as $item) {
$query->clone()->where('id', $item->id)->update([
'shared_secret' => $encrypter->$call($item->shared_secret),
'recovery_codes' => $item->recovery_codes ? $encrypter->$call($item->recovery_codes) : null,
]);
}
DB::commit();
}
);
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
// Before changing the shared secret column, we will need to decrypt the shared secret.
$this->chunkRows(false);

Schema::table('two_factor_authentications', static function (Blueprint $table): void {
$table->string('shared_secret')->change();
$table->json('recovery_codes')->nullable()->change();
});
}
}
Binary file removed laraguardlogo.png
Binary file not shown.
Binary file added logo.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 854560c

Please sign in to comment.