Skip to content

Instantly share code, notes, and snippets.

@dmandrade
Last active February 27, 2022 06:40
Show Gist options
  • Save dmandrade/ae0c0d3cb41d55ac475a0110f20b46e0 to your computer and use it in GitHub Desktop.
Save dmandrade/ae0c0d3cb41d55ac475a0110f20b46e0 to your computer and use it in GitHub Desktop.
Add views to confirm 2FA enable using TOTP code
Add 2FA enable confirmation using TOTP code
@package laravel/jetstream
--- src/Http/Livewire/TwoFactorAuthenticationForm.php
+++ src/Http/Livewire/TwoFactorAuthenticationForm.php
@@ -3,8 +3,9 @@
namespace Laravel\Jetstream\Http\Livewire;
use Illuminate\Support\Facades\Auth;
+use Laravel\Fortify\Actions\ConfirmEnableTwoFactorAuthentication;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
-use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
+use Laravel\Fortify\Actions\GenerateTwoFactorAuthenticationSecret;
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
use Laravel\Fortify\Features;
use Laravel\Jetstream\ConfirmsPasswords;
@@ -29,23 +30,47 @@ class TwoFactorAuthenticationForm extends Component
public $showingRecoveryCodes = false;
/**
- * Enable two factor authentication for the user.
+ * Code to confirm activation of two-factor authentication
+ * @var string
+ */
+ public $confirmationCode = null;
+
+ /**
+ * Generate two factor authentication secret for user.
*
- * @param \Laravel\Fortify\Actions\EnableTwoFactorAuthentication $enable
+ * @param \Laravel\Fortify\Actions\GenerateTwoFactorAuthenticationSecret $generate
* @return void
*/
- public function enableTwoFactorAuthentication(EnableTwoFactorAuthentication $enable)
+ public function generateTwoFactorAuthenticationSecret(GenerateTwoFactorAuthenticationSecret $generate)
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
$this->ensurePasswordIsConfirmed();
}
- $enable(Auth::user());
+ $generate(Auth::user());
$this->showingQrCode = true;
$this->showingRecoveryCodes = true;
}
+ /**
+ * Enable two factor authentication for the user.
+ *
+ * @param \Laravel\Fortify\Actions\ConfirmEnableTwoFactorAuthentication $enable
+ * @return void
+ */
+ public function confirmEnableTwoFactorAuthentication(ConfirmEnableTwoFactorAuthentication $enable)
+ {
+ $enabled = $enable(Auth::user(), $this->confirmationCode);
+
+ if (!$enabled) {
+ $this->addError('confirmationCode', __('The provided two factor authentication code was invalid.'));
+ return;
+ }
+
+ $this->hideSetup();
+ }
+
/**
* Display the user's recovery codes.
*
@@ -89,9 +114,17 @@ public function disableTwoFactorAuthentication(DisableTwoFactorAuthentication $d
$this->ensurePasswordIsConfirmed();
}
+ $this->hideSetup();
$disable(Auth::user());
}
+ protected function hideSetup()
+ {
+ $this->showingQrCode = false;
+ $this->showingRecoveryCodes = false;
+ $this->confirmationCode = null;
+ }
+
/**
* Get the current user of the application.
*
@@ -102,6 +135,16 @@ public function getUserProperty()
return Auth::user();
}
+ /**
+ * Determine if two-factor authentication is pending configuration.
+ *
+ * @return bool
+ */
+ public function getSetupProperty()
+ {
+ return ! empty($this->user->two_factor_secret) && !$this->user->is_two_factor_enabled;
+ }
+
/**
* Determine if two factor authentication is enabled.
*
@@ -109,7 +152,7 @@ public function getUserProperty()
*/
public function getEnabledProperty()
{
- return ! empty($this->user->two_factor_secret);
+ return ! empty($this->user->two_factor_secret) && $this->user->is_two_factor_enabled;
}
/**
--- src/Http/Middleware/ShareInertiaData.php
+++ src/Http/Middleware/ShareInertiaData.php
@@ -48,7 +48,8 @@ public function handle($request, $next)
return array_merge($request->user()->toArray(), array_filter([
'all_teams' => Jetstream::hasTeamFeatures() ? $request->user()->allTeams() : null,
]), [
- 'two_factor_enabled' => ! is_null($request->user()->two_factor_secret),
+ 'two_factor_enabled' => $request->user()->is_two_factor_enabled,
+ 'two_factor_setup' => ! is_null($request->user()->two_factor_secret),
]);
},
'errorBags' => function () {
--- stubs/inertia/resources/js/Pages/Profile/TwoFactorAuthenticationForm.vue
+++ stubs/inertia/resources/js/Pages/Profile/TwoFactorAuthenticationForm.vue
@@ -23,37 +23,44 @@
</p>
</div>
- <div v-if="twoFactorEnabled">
- <div v-if="qrCode">
- <div class="mt-4 max-w-xl text-sm text-gray-600">
- <p class="font-semibold">
- Two factor authentication is now enabled. Scan the following QR code using your phone's authenticator application.
- </p>
- </div>
+ <div v-if="recoveryCodes.length > 0">
+ <div class="mt-4 max-w-xl text-sm text-gray-600">
+ <p class="font-semibold">
+ Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.
+ </p>
+ </div>
- <div class="mt-4" v-html="qrCode">
+ <div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg">
+ <div v-for="code in recoveryCodes" :key="code">
+ {{ code }}
</div>
</div>
+ </div>
- <div v-if="recoveryCodes.length > 0">
- <div class="mt-4 max-w-xl text-sm text-gray-600">
- <p class="font-semibold">
- Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.
- </p>
- </div>
+ <div v-if="twoFactorSetup || displayQrCord">
+ <div class="mt-4 max-w-xl text-sm text-gray-600">
+ <p class="font-semibold">
+ Scan the following QR code using your phone\'s authenticator application to setup two factor authentication.
+ </p>
+ </div>
- <div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg">
- <div v-for="code in recoveryCodes" :key="code">
- {{ code }}
- </div>
+ <div class="mt-4" v-html="qrCode">
+ </div>
+ </div>
+
+ <div v-if="twoFactorSetup">
+ <div class="mt-4 max-w-xl text-sm text-gray-600">
+ <div class="col-span-6 sm:col-span-4">
+ <jet-label for="confirmationCode" value="After configuring the authenticator application, enter the code to validate the two-factor authentication." />
+ <jet-input id="confirmationCode" type="text" class="mt-1 block w-full" v-model="confirmationCode" />
</div>
</div>
</div>
<div class="mt-5">
- <div v-if="! twoFactorEnabled">
- <jet-confirms-password @confirmed="enableTwoFactorAuthentication">
- <jet-button type="button" :class="{ 'opacity-25': enabling }" :disabled="enabling">
+ <div v-if="! twoFactorEnabled && ! twoFactorSetup">
+ <jet-confirms-password @confirmed="setupTwoFactorAuthentication">
+ <jet-button type="button" :class="{ 'opacity-25': setuping }" :disabled="setuping">
Enable
</jet-button>
</jet-confirms-password>
@@ -73,13 +80,22 @@
</jet-secondary-button>
</jet-confirms-password>
- <jet-confirms-password @confirmed="disableTwoFactorAuthentication">
- <jet-danger-button
- :class="{ 'opacity-25': disabling }"
- :disabled="disabling">
- Disable
- </jet-danger-button>
- </jet-confirms-password>
+ <div v-if="twoFactorEnabled">
+ <jet-button
+ :class="{ 'opacity-25': enabling }"
+ :disabled="enabling"
+ @click="enableTwoFactorAuthentication">
+ Confirm
+ </jet-button>
+ <div v-else>
+ <jet-confirms-password @confirmed="disableTwoFactorAuthentication">
+ <jet-danger-button
+ :class="{ 'opacity-25': disabling }"
+ :disabled="disabling">
+ Disable
+ </jet-danger-button>
+ </jet-confirms-password>
+ <div>
</div>
</div>
</template>
@@ -105,16 +121,18 @@
data() {
return {
enabling: false,
+ setuping: false,
disabling: false,
qrCode: null,
recoveryCodes: [],
+ confirmationCode: null,
}
},
methods: {
- enableTwoFactorAuthentication() {
- this.enabling = true
+ setupTwoFactorAuthentication() {
+ this.setuping = true
this.$inertia.post('/user/two-factor-authentication', {}, {
preserveScroll: true,
@@ -122,6 +140,19 @@
this.showQrCode(),
this.showRecoveryCodes(),
]),
+ onFinish: () => (this.setuping = false),
+ })
+ },
+ enableTwoFactorAuthentication() {
+ this.setuping = false
+ this.enabling = true
+
+ this.$inertia.post('/user/two-factor-authentication/confirm', {code: this.confirmationCode}, {
+ preserveScroll: true,
+ onSuccess: () => {
+ this.qrCode = null;
+ this.recoveryCodes = [];
+ },
onFinish: () => (this.enabling = false),
})
},
@@ -160,6 +191,9 @@
computed: {
twoFactorEnabled() {
return ! this.enabling && this.$page.props.user.two_factor_enabled
+ },
+ twoFactorSetup() {
+ return this.setuping || this.$page.props.user.two_factor_setup
}
}
}
--- stubs/livewire/resources/views/profile/two-factor-authentication-form.blade.php
+++ stubs/livewire/resources/views/profile/two-factor-authentication-form.blade.php
@@ -22,37 +22,45 @@
</p>
</div>
- @if ($this->enabled)
- @if ($showingQrCode)
- <div class="mt-4 max-w-xl text-sm text-gray-600">
- <p class="font-semibold">
- {{ __('Two factor authentication is now enabled. Scan the following QR code using your phone\'s authenticator application.') }}
- </p>
- </div>
+ @if ($showingRecoveryCodes)
+ <div class="mt-4 max-w-xl text-sm text-gray-600">
+ <p class="font-semibold">
+ {{ __('Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.') }}
+ </p>
+ </div>
- <div class="mt-4">
- {!! $this->user->twoFactorQrCodeSvg() !!}
- </div>
- @endif
+ <div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg">
+ @foreach (json_decode(decrypt($this->user->two_factor_recovery_codes), true) as $code)
+ <div>{{ $code }}</div>
+ @endforeach
+ </div>
+ @endif
- @if ($showingRecoveryCodes)
- <div class="mt-4 max-w-xl text-sm text-gray-600">
- <p class="font-semibold">
- {{ __('Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.') }}
- </p>
- </div>
+ @if ($this->setup || $showingQrCode)
+ <div class="mt-4 max-w-xl text-sm text-gray-600">
+ <p class="font-semibold">
+ {{ __('Scan the following QR code using your phone\'s authenticator application to setup two factor authentication.') }}
+ </p>
+ </div>
+
+ <div class="mt-4 dark:p-4 dark:w-56 dark:bg-white">
+ {!! $this->user->twoFactorQrCodeSvg() !!}
+ </div>
+ @endif
- <div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg">
- @foreach (json_decode(decrypt($this->user->two_factor_recovery_codes), true) as $code)
- <div>{{ $code }}</div>
- @endforeach
+ @if ($this->setup)
+ <div class="mt-4 max-w-xl text-sm text-gray-600">
+ <div class="col-span-6 sm:col-span-4">
+ <x-jet-label for="confirmationCode" value="{{ __('After configuring the authenticator application, enter the code to validate the two-factor authentication.') }}" />
+ <x-jet-input id="confirmationCode" type="text" class="mt-1 block w-full" wire:model.defer="confirmationCode" />
+ <x-jet-input-error for="confirmationCode" class="mt-2" />
</div>
- @endif
+ </div>
@endif
<div class="mt-5">
- @if (! $this->enabled)
- <x-jet-confirms-password wire:then="enableTwoFactorAuthentication">
+ @if (! $this->setup && !$this->enabled)
+ <x-jet-confirms-password wire:then="generateTwoFactorAuthenticationSecret">
<x-jet-button type="button" wire:loading.attr="disabled">
{{ __('Enable') }}
</x-jet-button>
@@ -72,11 +80,17 @@
</x-jet-confirms-password>
@endif
- <x-jet-confirms-password wire:then="disableTwoFactorAuthentication">
- <x-jet-danger-button wire:loading.attr="disabled">
- {{ __('Disable') }}
- </x-jet-danger-button>
- </x-jet-confirms-password>
+ @if($this->enabled)
+ <x-jet-confirms-password wire:then="disableTwoFactorAuthentication">
+ <x-jet-danger-button wire:loading.attr="disabled">
+ {{ __('Disable') }}
+ </x-jet-danger-button>
+ </x-jet-confirms-password>
+ @else
+ <x-jet-button wire:click="confirmEnableTwoFactorAuthentication" wire:loading.attr="disabled">
+ {{ __('Confirm') }}
+ </x-jet-button>
+ @endif
@endif
</div>
</x-slot>
@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

To use it, just install this patch with another one specific for fortify

After installing both patches update your resources according to the stack used using as a reference the files present in vendor/laravel/jetstream/stubs

Livewire - update file:

/resources/views/profile/two-factor-authentication-form.blade.php

Inertia - update file:

/resources/js/Pages/Profile/TwoFactorAuthenticationForm.vue

OBS: I didn't get to test the solution with jetstream + inertia, but with livewire works well

@MrReeds
Copy link

MrReeds commented Aug 31, 2021

Updating to latest version caused an error.
Changed the

--- stubs/inertia/resources/js/Pages/Profile/TwoFactorAuthenticationForm.vue
+++ stubs/inertia/resources/js/Pages/Profile/TwoFactorAuthenticationForm.vue

to

--- stubs/inertia/resources/js/Pages/Profile/Partials/TwoFactorAuthenticationForm.vue
+++ stubs/inertia/resources/js/Pages/Profile/Partials/TwoFactorAuthenticationForm.vue

and got it working, but i unfortunately did not check if patch still works as intended otherwise

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