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>