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 1 commit
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
Prev Previous commit
Next Next commit
inertia support
  • Loading branch information
taylorotwell committed Mar 15, 2022
commit 232f025635a2ad2839d86412175805bc3c657262
33 changes: 33 additions & 0 deletions src/Http/Controllers/Inertia/UserProfileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
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
Expand All @@ -19,7 +22,37 @@ class UserProfileController extends Controller
*/
public function show(Request $request)
{
$currentTime = time();

// Notate totally disabled state in session...
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm') &&
is_null(Auth::user()->two_factor_secret) &&
is_null(Auth::user()->two_factor_confirmed_at)) {
$request->session()->put('two_factor_empty_at', $currentTime);
}

// If was previously totally disabled this session but is now confirming, notate time...
if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm') &&
! is_null(Auth::user()->two_factor_secret) &&
is_null(Auth::user()->two_factor_confirmed_at) &&
$request->session()->has('two_factor_empty_at') &&
is_null($request->session()->get('two_factor_confirming_at'))) {
$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 (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm') &&
is_null(Auth::user()->two_factor_confirmed_at) &&
// Don't disable if confirmation was first noted during this same request...
$request->session()->get('two_factor_confirming_at', 0) != $currentTime) {
app(DisableTwoFactorAuthentication::class)(Auth::user());

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

return Jetstream::inertia()->render($request, 'Profile/Show', [
'confirmsTwoFactorAuthentication' => Features::optionEnabled(Features::twoFactorAuthentication(), 'confirm'),
'sessions' => $this->sessions($request)->all(),
]);
}
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
9 changes: 7 additions & 2 deletions stubs/inertia/resources/js/Pages/Profile/Show.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
</div>

<div v-if="$page.props.jetstream.canManageTwoFactorAuthentication">
<two-factor-authentication-form class="mt-10 sm:mt-0" />
<two-factor-authentication-form
class="mt-10 sm:mt-0"
:requires-confirmation="confirmsTwoFactorAuthentication" />

<jet-section-border />
</div>
Expand Down Expand Up @@ -49,7 +51,10 @@
import UpdateProfileInformationForm from '@/Pages/Profile/Partials/UpdateProfileInformationForm.vue'

export default defineComponent({
props: ['sessions'],
props: [
'confirmsTwoFactorAuthentication',
'sessions'
],

components: {
AppLayout,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<div class="mt-4">
<x-jet-label for="code" value="{{ __('Code') }}" />

<x-jet-input id="code" class="block mt-1 w-1/2" type="text" inputmode="numeric" name="code" autofocus autocomplete="one-time-code"
<x-jet-input id="code" type="text" name="code" class="block mt-1 w-1/2" inputmode="numeric" autofocus autocomplete="one-time-code"
wire:model.defer="code"
wire:keydown.enter="confirmTwoFactorAuthentication" />

Expand Down