Skip to content

Instantly share code, notes, and snippets.

@dmandrade
Created June 2, 2021 14:22
Show Gist options
  • Save dmandrade/092818de02c7987ba2615448f0428382 to your computer and use it in GitHub Desktop.
Save dmandrade/092818de02c7987ba2615448f0428382 to your computer and use it in GitHub Desktop.
Adds option to confirm activation of 2FA
Adds option to confirm activation of 2FA
@package laravel/fortify
--- /dev/null
+++ database/migrations/2021_06_01_145800_add_two_factor_confirmed_column_to_users_table.php
@@ -0,0 +1,34 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class AddTwoFactorConfirmedColumnToUsersTable extends Migration
+{
+ /**
+ * Run the migrations.
+ *
+ * @return void
+ */
+ public function up()
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->boolean('two_factor_confirmed')
+ ->after('two_factor_recovery_codes')
+ ->default(false);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropColumn('two_factor_confirmed');
+ });
+ }
+}
--- routes/routes.php
+++ routes/routes.php
@@ -140,6 +140,10 @@
->middleware($twoFactorMiddleware)
->name('two-factor.enable');
+ Route::post('/user/two-factor-authentication/confirm', [TwoFactorAuthenticationController::class, 'confirm'])
+ ->middleware($twoFactorMiddleware)
+ ->name('two-factor.confirm');
+
Route::delete('/user/two-factor-authentication', [TwoFactorAuthenticationController::class, 'destroy'])
->middleware($twoFactorMiddleware)
->name('two-factor.disable');
--- /dev/null
+++ src/Actions/ConfirmEnableTwoFactorAuthentication.php
@@ -0,0 +1,45 @@
+<?php
+
+namespace Laravel\Fortify\Actions;
+
+use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider;
+
+class ConfirmEnableTwoFactorAuthentication
+{
+ /**
+ * The two factor authentication provider.
+ *
+ * @var \Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider
+ */
+ protected $provider;
+
+ /**
+ * Create a new action instance.
+ *
+ * @param \Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider $provider
+ * @return void
+ */
+ public function __construct(TwoFactorAuthenticationProvider $provider)
+ {
+ $this->provider = $provider;
+ }
+
+ /**
+ * Enable two factor authentication for the user.
+ *
+ * @param \Illuminate\Foundation\Auth\User $user
+ * @param string $code
+ * @return bool
+ */
+ public function __invoke($user, $code)
+ {
+ if (! $this->provider->verify(decrypt($user->two_factor_secret), $code)) {
+ return false;
+ }
+
+ $user->two_factor_confirmed = true;
+ $user->save();
+
+ return true;
+ }
+}
--- src/Actions/DisableTwoFactorAuthentication.php
+++ src/Actions/DisableTwoFactorAuthentication.php
@@ -13,6 +13,7 @@ class DisableTwoFactorAuthentication
public function __invoke($user)
{
$user->forceFill([
+ 'two_factor_confirmed' => false,
'two_factor_secret' => null,
'two_factor_recovery_codes' => null,
])->save();
--- src/Actions/EnableTwoFactorAuthentication.php
+++ src/Actions/EnableTwoFactorAuthentication.php
@@ -2,43 +2,9 @@
namespace Laravel\Fortify\Actions;
-use Illuminate\Support\Collection;
-use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider;
-use Laravel\Fortify\RecoveryCode;
-
-class EnableTwoFactorAuthentication
+/**
+ * @deprecated
+ */
+class EnableTwoFactorAuthentication extends GenerateTwoFactorAuthenticationSecret
{
- /**
- * The two factor authentication provider.
- *
- * @var \Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider
- */
- protected $provider;
-
- /**
- * Create a new action instance.
- *
- * @param \Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider $provider
- * @return void
- */
- public function __construct(TwoFactorAuthenticationProvider $provider)
- {
- $this->provider = $provider;
- }
-
- /**
- * Enable two factor authentication for the user.
- *
- * @param mixed $user
- * @return void
- */
- public function __invoke($user)
- {
- $user->forceFill([
- 'two_factor_secret' => encrypt($this->provider->generateSecretKey()),
- 'two_factor_recovery_codes' => encrypt(json_encode(Collection::times(8, function () {
- return RecoveryCode::generate();
- })->all())),
- ])->save();
- }
}
--- /dev/null
+++ src/Actions/GenerateTwoFactorAuthenticationSecret.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Laravel\Fortify\Actions;
+
+use Illuminate\Support\Collection;
+use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider;
+use Laravel\Fortify\RecoveryCode;
+
+class GenerateTwoFactorAuthenticationSecret
+{
+ /**
+ * The two factor authentication provider.
+ *
+ * @var \Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider
+ */
+ protected $provider;
+
+ /**
+ * Create a new action instance.
+ *
+ * @param \Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider $provider
+ * @return void
+ */
+ public function __construct(TwoFactorAuthenticationProvider $provider)
+ {
+ $this->provider = $provider;
+ }
+
+ /**
+ * Generate two factor authentication secret and recovery codes for the user.
+ *
+ * @param mixed $user
+ * @return void
+ */
+ public function __invoke($user)
+ {
+ $user->forceFill([
+ 'two_factor_secret' => encrypt($this->provider->generateSecretKey()),
+ 'two_factor_recovery_codes' => encrypt(json_encode(Collection::times(8, function () {
+ return RecoveryCode::generate();
+ })->all())),
+ ])->save();
+ }
+}
--- src/Actions/RedirectIfTwoFactorAuthenticatable.php
+++ src/Actions/RedirectIfTwoFactorAuthenticatable.php
@@ -49,7 +49,7 @@ public function handle($request, $next)
{
$user = $this->validateCredentials($request);
- if (optional($user)->two_factor_secret &&
+ if (optional($user)->is_two_factor_enabled &&
in_array(TwoFactorAuthenticatable::class, class_uses_recursive($user))) {
return $this->twoFactorChallengeResponse($request, $user);
}
--- src/Http/Controllers/TwoFactorAuthenticationController.php
+++ src/Http/Controllers/TwoFactorAuthenticationController.php
@@ -5,8 +5,9 @@
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
+use Laravel\Fortify\Actions\ConfirmEnableTwoFactorAuthentication;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
-use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
+use Laravel\Fortify\Actions\GenerateTwoFactorAuthenticationSecret;
class TwoFactorAuthenticationController extends Controller
{
@@ -14,18 +15,40 @@ class TwoFactorAuthenticationController extends Controller
* Enable two factor authentication for the user.
*
* @param \Illuminate\Http\Request $request
- * @param \Laravel\Fortify\Actions\EnableTwoFactorAuthentication $enable
+ * @param \Laravel\Fortify\Actions\GenerateTwoFactorAuthenticationSecret $enable
* @return \Symfony\Component\HttpFoundation\Response
*/
- public function store(Request $request, EnableTwoFactorAuthentication $enable)
+ public function store(Request $request, GenerateTwoFactorAuthenticationSecret $generate)
{
- $enable($request->user());
+ $generate($request->user());
return $request->wantsJson()
? new JsonResponse('', 200)
: back()->with('status', 'two-factor-authentication-enabled');
}
+ /**
+ * Confirms activation of two-factor authentication by validating a TOTP code.
+ *
+ * @param \Illuminate\Http\Request $request
+ * @param \Laravel\Fortify\Actions\ConfirmEnableTwoFactorAuthentication $confirm
+ * @return \Symfony\Component\HttpFoundation\Response
+ */
+ public function confirm(Request $request, ConfirmEnableTwoFactorAuthentication $enable)
+ {
+ if (! $enable($request->user(), $request->code)) {
+ $error = __('The provided two factor authentication code was invalid.');
+
+ return $request->wantsJson()
+ ? new JsonResponse($error, 422)
+ : back()->withErrors($error);
+ }
+
+ return $request->wantsJson()
+ ? new JsonResponse('', 200)
+ : back();
+ }
+
/**
* Disable two factor authentication for the user.
*
--- src/TwoFactorAuthenticatable.php
+++ src/TwoFactorAuthenticatable.php
@@ -22,6 +22,20 @@ public function recoveryCodes()
return json_decode(decrypt($this->two_factor_recovery_codes), true);
}
+ /**
+ * Check if two factor authentication is enabled for user.
+ *
+ * @return string|bool
+ */
+ public function getIsTwoFactorEnabledAttribute()
+ {
+ if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmTwoFactor')) {
+ return ! empty($this->two_factor_secret) && $this->two_factor_confirmed;
+ }
+
+ return ! empty($this->two_factor_secret);
+ }
+
/**
* Replace the given recovery code with a new one in the user's stored codes.
*
--- stubs/fortify.php
+++ stubs/fortify.php
@@ -139,6 +139,7 @@
Features::updatePasswords(),
Features::twoFactorAuthentication([
'confirmPassword' => true,
+ 'confirmTwoFactor' => true,
]),
],
@dmandrade
Copy link
Author

dmandrade commented Jun 2, 2021

To apply it automatically first install the vaimo/composer-patches package.

Save this gist file in PROJECT_ROOT/patches folder.

In composer.json add:

    "extra": {
        ...
        "patcher": {
            "search": "patches"
        }
    }

After adding the patch file and update composer.json run composer patch:apply

Then publish the new fortify config and migration

php artisan vendor:publish --tag=fortify-config
php artisan vendor:publish --tag=fortify-migrations
php artisan migrate

@Patrick-Shard
Copy link

Hey,
I've tried this out in a demo project. However, I have run into a problem with applying the patch.

Steps I have taken:

  1. Require laravel/fortify
  2. Require vaimo/composer-patches
  3. Edit the composer.json
  4. composer patch:apply
  - Applying patches for laravel/fortify (1)
    ~ laravel/laravel: patches\fortify-confirm-2fa.patch [NEW]
      Adds option to confirm activation of 2FA
      Failed to apply the patch. Halting execution!```
      
Do you have any idea what I might be doing wrong?
  

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment