Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2.x] Confirm 2FA when enabling #992

Merged
merged 7 commits into from
Mar 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"ext-json": "*",
"illuminate/support": "^8.37|^9.0",
"jenssegers/agent": "^2.6",
"laravel/fortify": "^1.9"
"laravel/fortify": "^1.11.1"
},
"require-dev": {
"inertiajs/inertia-laravel": "^0.5.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

namespace Laravel\Jetstream\Http\Controllers\Inertia\Concerns;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Features;

trait ConfirmsTwoFactorAuthentication
{
/**
* Validate the two factor authentication state for the request.
*
* @param \Illuminate\Http\Request
* @return void
*/
protected function validateTwoFactorAuthenticationState(Request $request)
{
if (! Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm')) {
return;
}

$currentTime = time();

// Notate totally disabled state in session...
if ($this->twoFactorAuthenticationDisabled($request)) {
$request->session()->put('two_factor_empty_at', $currentTime);
}

// If was previously totally disabled this session but is now confirming, notate time...
if ($this->hasJustBegunConfirmingTwoFactorAuthentication($request)) {
$request->session()->put('two_factor_confirming_at', $currentTime);
}

// If the profile is reloaded and is not confirmed but was previously in confirming state, disable...
if ($this->neverFinishedConfirmingTwoFactorAuthentication($request, $currentTime)) {
app(DisableTwoFactorAuthentication::class)(Auth::user());

$request->session()->put('two_factor_empty_at', $currentTime);
$request->session()->remove('two_factor_confirming_at');
}
}

/**
* Determine if two factor authenticatoin is totally disabled.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function twoFactorAuthenticationDisabled(Request $request)
{
return is_null($request->user()->two_factor_secret) &&
is_null($request->user()->two_factor_confirmed_at);
}

/**
* Determine if two factor authentication is just now being confirmed within the last request cycle.
*
* @param \Illuminate\Http\Request $request
* @return bool
*/
protected function hasJustBegunConfirmingTwoFactorAuthentication(Request $request)
{
return ! is_null($request->user()->two_factor_secret) &&
is_null($request->user()->two_factor_confirmed_at) &&
$request->session()->has('two_factor_empty_at') &&
is_null($request->session()->get('two_factor_confirming_at'));
}

/**
* Determine if two factor authentication was never totally confirmed once confirmation started.
*
* @param \Illuminate\Http\Request $request
* @param int $currentTime
* @return bool
*/
protected function neverFinishedConfirmingTwoFactorAuthentication(Request $request, $currentTime)
{
return is_null($request->user()->two_factor_confirmed_at) &&
$request->session()->get('two_factor_confirming_at', 0) != $currentTime;
}
}
8 changes: 8 additions & 0 deletions src/Http/Controllers/Inertia/UserProfileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Jenssegers\Agent\Agent;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Features;
use Laravel\Jetstream\Jetstream;

class UserProfileController extends Controller
{
use Concerns\ConfirmsTwoFactorAuthentication;

/**
* Show the general profile settings screen.
*
Expand All @@ -19,7 +24,10 @@ class UserProfileController extends Controller
*/
public function show(Request $request)
{
$this->validateTwoFactorAuthenticationState($request);

return Jetstream::inertia()->render($request, 'Profile/Show', [
'confirmsTwoFactorAuthentication' => Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'),
'sessions' => $this->sessions($request)->all(),
]);
}
Expand Down
56 changes: 56 additions & 0 deletions src/Http/Livewire/TwoFactorAuthenticationForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Laravel\Jetstream\Http\Livewire;

use Illuminate\Support\Facades\Auth;
use Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication;
use Laravel\Fortify\Actions\DisableTwoFactorAuthentication;
use Laravel\Fortify\Actions\EnableTwoFactorAuthentication;
use Laravel\Fortify\Actions\GenerateNewRecoveryCodes;
Expand All @@ -21,13 +22,40 @@ class TwoFactorAuthenticationForm extends Component
*/
public $showingQrCode = false;

/**
* Indicates if the two factor authentication confirmation input and button are being displayed.
*
* @var bool
*/
public $showingConfirmation = false;

/**
* Indicates if two factor authentication recovery codes are being displayed.
*
* @var bool
*/
public $showingRecoveryCodes = false;

/**
* The OTP code for confirming two factor authentication.
*
* @var string|null
*/
public $code;

/**
* Mount the component.
*
* @return void
*/
public function mount()
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm') &&
is_null(Auth::user()->two_factor_confirmed_at)) {
app(DisableTwoFactorAuthentication::class)(Auth::user());
}
}

/**
* Enable two factor authentication for the user.
*
Expand All @@ -43,6 +71,30 @@ public function enableTwoFactorAuthentication(EnableTwoFactorAuthentication $ena
$enable(Auth::user());

$this->showingQrCode = true;

if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm')) {
$this->showingConfirmation = true;
} else {
$this->showingRecoveryCodes = true;
}
}

/**
* Confirm two factor authentication for the user.
*
* @param \Laravel\Fortify\Actions\ConfirmTwoFactorAuthentication $confirm
* @return void
*/
public function confirmTwoFactorAuthentication(ConfirmTwoFactorAuthentication $confirm)
{
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) {
$this->ensurePasswordIsConfirmed();
}

$confirm(Auth::user(), $this->code);

$this->showingQrCode = false;
$this->showingConfirmation = false;
$this->showingRecoveryCodes = true;
}

Expand Down Expand Up @@ -90,6 +142,10 @@ public function disableTwoFactorAuthentication(DisableTwoFactorAuthentication $d
}

$disable(Auth::user());

$this->showingQrCode = false;
$this->showingConfirmation = false;
$this->showingRecoveryCodes = false;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@
</template>

<template #content>
<h3 class="text-lg font-medium text-gray-900" v-if="twoFactorEnabled">
<h3 class="text-lg font-medium text-gray-900" v-if="twoFactorEnabled && ! confirming">
You have enabled two factor authentication.
</h3>

<h3 class="text-lg font-medium text-gray-900" v-else-if="confirming">
Finish enabling two factor authentication.
</h3>

<h3 class="text-lg font-medium text-gray-900" v-else>
You have not enabled two factor authentication.
</h3>
Expand All @@ -26,16 +30,34 @@
<div v-if="twoFactorEnabled">
<div v-if="qrCode">
<div class="mt-4 max-w-xl text-sm text-gray-600">
<p class="font-semibold">
<p class="font-semibold" v-if="confirming">
To finish enabling two factor authentication, scan the following QR code using your phone's authenticator application and provide the generated OTP code.
</p>

<p v-else>
Two factor authentication is now enabled. Scan the following QR code using your phone's authenticator application.
</p>
</div>

<div class="mt-4" v-html="qrCode">
</div>

<div class="mt-4" v-if="confirming">
<jet-label for="code" value="Code" />

<jet-input id="code" type="text" name="code"
class="block mt-1 w-1/2"
inputmode="numeric"
autofocus
autocomplete="one-time-code"
v-model="confirmationForm.code"
@keyup.enter="confirmTwoFactorAuthentication" />

<jet-input-error :message="confirmationForm.errors.code" class="mt-2" />
</div>
</div>

<div v-if="recoveryCodes.length > 0">
<div v-if="recoveryCodes.length > 0 && ! confirming">
<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.
Expand All @@ -60,23 +82,39 @@
</div>

<div v-else>
<jet-confirms-password @confirmed="confirmTwoFactorAuthentication">
<jet-button type="button" class="mr-3" :class="{ 'opacity-25': enabling }" :disabled="enabling" v-if="confirming">
Confirm
</jet-button>
</jet-confirms-password>

<jet-confirms-password @confirmed="regenerateRecoveryCodes">
<jet-secondary-button class="mr-3"
v-if="recoveryCodes.length > 0">
v-if="recoveryCodes.length > 0 && ! confirming">
Regenerate Recovery Codes
</jet-secondary-button>
</jet-confirms-password>

<jet-confirms-password @confirmed="showRecoveryCodes">
<jet-secondary-button class="mr-3" v-if="recoveryCodes.length === 0">
<jet-secondary-button class="mr-3" v-if="recoveryCodes.length === 0 && ! confirming">
Show Recovery Codes
</jet-secondary-button>
</jet-confirms-password>

<jet-confirms-password @confirmed="disableTwoFactorAuthentication">
<jet-secondary-button
:class="{ 'opacity-25': disabling }"
:disabled="disabling"
v-if="confirming">
Cancel
</jet-secondary-button>
</jet-confirms-password>

<jet-confirms-password @confirmed="disableTwoFactorAuthentication">
<jet-danger-button
:class="{ 'opacity-25': disabling }"
:disabled="disabling">
:disabled="disabling"
v-if="! confirming">
Disable
</jet-danger-button>
</jet-confirms-password>
Expand All @@ -92,6 +130,9 @@
import JetButton from '@/Jetstream/Button.vue'
import JetConfirmsPassword from '@/Jetstream/ConfirmsPassword.vue'
import JetDangerButton from '@/Jetstream/DangerButton.vue'
import JetInput from '@/Jetstream/Input.vue'
import JetInputError from '@/Jetstream/InputError.vue'
import JetLabel from '@/Jetstream/Label.vue'
import JetSecondaryButton from '@/Jetstream/SecondaryButton.vue'

export default defineComponent({
Expand All @@ -100,16 +141,26 @@
JetButton,
JetConfirmsPassword,
JetDangerButton,
JetInput,
JetInputError,
JetLabel,
JetSecondaryButton,
},

props: ['requiresConfirmation'],

data() {
return {
enabling: false,
confirming: false,
disabling: false,

qrCode: null,
recoveryCodes: [],

confirmationForm: this.$inertia.form({
code: '',
}),
}
},

Expand All @@ -123,7 +174,10 @@
this.showQrCode(),
this.showRecoveryCodes(),
]),
onFinish: () => (this.enabling = false),
onFinish: () => {
this.enabling = false
this.confirming = this.requiresConfirmation
}
})
},

Expand All @@ -141,6 +195,17 @@
})
},

confirmTwoFactorAuthentication() {
this.confirmationForm.post('/user/confirmed-two-factor-authentication', {
preserveScroll: true,
preserveState: true,
onSuccess: () => {
this.confirming = false
this.qrCode = null
}
})
},

regenerateRecoveryCodes() {
axios.post('/user/two-factor-recovery-codes')
.then(response => {
Expand All @@ -153,7 +218,10 @@

this.$inertia.delete('/user/two-factor-authentication', {
preserveScroll: true,
onSuccess: () => (this.disabling = false),
onSuccess: () => {
this.disabling = false
this.confirming = false
}
})
},
},
Expand Down
Loading