diff --git a/app/Actions/Fortify/ConfirmPassword.php b/app/Actions/Fortify/ConfirmPassword.php new file mode 100644 index 0000000..07d7c87 --- /dev/null +++ b/app/Actions/Fortify/ConfirmPassword.php @@ -0,0 +1,52 @@ +guard = $guard; + $this->limiter = $limiter; + } + /** + * Confirm that the given password is valid for the given user. + * + * @param \Illuminate\Contracts\Auth\StatefulGuard $guard + * @param mixed $user + * @param string|null $password + * @return bool + */ + public function __invoke(StatefulGuard $guard, $user, ?string $password = null) + { + + $username = config('fortify.username'); + + return is_null(Fortify::$confirmPasswordsUsingCallback) ? $this->guard->validate([ + $username => $user->{$username}, + 'password' => $password, + ]) : $this->confirmPasswordUsingCustomCallback($user, $password); + } + + /** + * Confirm the user's password using a custom callback. + * + * @param mixed $user + * @param string|null $password + * @return bool + */ + protected function confirmPasswordUsingCustomCallback($user, ?string $password = null) + { + return call_user_func( + Fortify::$confirmPasswordsUsingCallback, + $user, + $password + ); + } +} diff --git a/app/Actions/Fortify/Controllers/TwoFactorAuthenticatedSessionController.php b/app/Actions/Fortify/Controllers/TwoFactorAuthenticatedSessionController.php new file mode 100644 index 0000000..0444c26 --- /dev/null +++ b/app/Actions/Fortify/Controllers/TwoFactorAuthenticatedSessionController.php @@ -0,0 +1,73 @@ +guard = $guard; + } + + /** + * Show the two factor authentication challenge view. + * + * @param \Laravel\Fortify\Http\Requests\TwoFactorLoginRequest $request + * @return \Laravel\Fortify\Contracts\TwoFactorChallengeViewResponse + */ + public function create(TwoFactorLoginRequest $request): TwoFactorChallengeViewResponse + { + if (! $request->hasChallengedUser()) { + throw new HttpResponseException(redirect()->route('admin.login')); + } + + return app(TwoFactorChallengeViewResponse::class); + } + + /** + * Attempt to authenticate a new session using the two factor authentication code. + * + * @param \Laravel\Fortify\Http\Requests\TwoFactorLoginRequest $request + * @return mixed + */ + public function store(TwoFactorLoginRequest $request) + { + $user = $request->challengedUser(); + + if ($code = $request->validRecoveryCode()) { + $user->replaceRecoveryCode($code); + + event(new RecoveryCodeReplaced($user, $code)); + } elseif (! $request->hasValidCode()) { + return app(FailedTwoFactorLoginResponse::class)->toResponse($request); + } + + $this->guard->login($user, $request->remember()); + + $request->session()->regenerate(); + + return app(TwoFactorLoginResponse::class); + } +} diff --git a/app/Actions/Fortify/Controllers/TwoFactorAuthenticationController.php b/app/Actions/Fortify/Controllers/TwoFactorAuthenticationController.php new file mode 100644 index 0000000..121876d --- /dev/null +++ b/app/Actions/Fortify/Controllers/TwoFactorAuthenticationController.php @@ -0,0 +1,44 @@ +user()); + + return $request->wantsJson() + ? new JsonResponse('', 200) + : back()->with('status', 'two-factor-authentication-enabled'); + } + + /** + * Disable two factor authentication for the user. + * + * @param \Illuminate\Http\Request $request + * @param \Laravel\Fortify\Actions\DisableTwoFactorAuthentication $disable + * @return \Symfony\Component\HttpFoundation\Response + */ + public function destroy(Request $request, DisableTwoFactorAuthentication $disable) + { + $disable($request->user()); + + return $request->wantsJson() + ? new JsonResponse('', 200) + : back()->with('status', 'two-factor-authentication-disabled'); + } +} diff --git a/app/Actions/Fortify/RedirectIfTwoFactorAuthenticatable.php b/app/Actions/Fortify/RedirectIfTwoFactorAuthenticatable.php index 10486d6..c29b6c0 100644 --- a/app/Actions/Fortify/RedirectIfTwoFactorAuthenticatable.php +++ b/app/Actions/Fortify/RedirectIfTwoFactorAuthenticatable.php @@ -4,6 +4,7 @@ use Illuminate\Auth\Events\Failed; use Illuminate\Contracts\Auth\StatefulGuard; +use Illuminate\Support\Facades\Auth; use Illuminate\Validation\ValidationException; use Laravel\Fortify\Events\TwoFactorAuthenticationChallenged; use Laravel\Fortify\Fortify; @@ -29,12 +30,13 @@ class RedirectIfTwoFactorAuthenticatable /** * Create a new controller instance. * - * @param \Illuminate\Contracts\Auth\StatefulGuard $guard - * @param \Laravel\Fortify\LoginRateLimiter $limiter + * @param \Illuminate\Contracts\Auth\StatefulGuard $guard + * @param \Laravel\Fortify\LoginRateLimiter $limiter * @return void */ public function __construct(StatefulGuard $guard, LoginRateLimiter $limiter) { + $this->guard = $guard; $this->limiter = $limiter; } @@ -42,8 +44,8 @@ public function __construct(StatefulGuard $guard, LoginRateLimiter $limiter) /** * Handle the incoming request. * - * @param \Illuminate\Http\Request $request - * @param callable $next + * @param \Illuminate\Http\Request $request + * @param callable $next * @return mixed */ public function handle($request, $next) @@ -52,7 +54,7 @@ public function handle($request, $next) if (Fortify::confirmsTwoFactorAuthentication()) { if (optional($user)->two_factor_secret && - ! is_null(optional($user)->two_factor_confirmed_at) && + !is_null(optional($user)->two_factor_confirmed_at) && in_array(TwoFactorAuthenticatable::class, class_uses_recursive($user))) { return $this->twoFactorChallengeResponse($request, $user); } else { @@ -71,14 +73,14 @@ public function handle($request, $next) /** * Attempt to validate the incoming credentials. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return mixed */ protected function validateCredentials($request) { if (Fortify::$authenticateUsingCallback) { return tap(call_user_func(Fortify::$authenticateUsingCallback, $request), function ($user) use ($request) { - if (! $user) { + if (!$user) { $this->fireFailedEvent($request); $this->throwFailedAuthenticationException($request); @@ -89,7 +91,7 @@ protected function validateCredentials($request) $model = $this->guard->getProvider()->getModel(); return tap($model::where(Fortify::username(), $request->{Fortify::username()})->first(), function ($user) use ($request) { - if (! $user || ! $this->guard->getProvider()->validateCredentials($user, ['password' => $request->password])) { + if (!$user || !$this->guard->getProvider()->validateCredentials($user, ['password' => $request->password])) { $this->fireFailedEvent($request, $user); $this->throwFailedAuthenticationException($request); @@ -100,7 +102,7 @@ protected function validateCredentials($request) /** * Throw a failed authentication validation exception. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return void * * @throws \Illuminate\Validation\ValidationException @@ -117,8 +119,8 @@ protected function throwFailedAuthenticationException($request) /** * Fire the failed authentication attempt event with the given arguments. * - * @param \Illuminate\Http\Request $request - * @param \Illuminate\Contracts\Auth\Authenticatable|null $user + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Contracts\Auth\Authenticatable|null $user * @return void */ protected function fireFailedEvent($request, $user = null) @@ -132,8 +134,8 @@ protected function fireFailedEvent($request, $user = null) /** * Get the two factor authentication enabled response. * - * @param \Illuminate\Http\Request $request - * @param mixed $user + * @param \Illuminate\Http\Request $request + * @param mixed $user * @return \Symfony\Component\HttpFoundation\Response */ protected function twoFactorChallengeResponse($request, $user) @@ -145,8 +147,16 @@ protected function twoFactorChallengeResponse($request, $user) TwoFactorAuthenticationChallenged::dispatch($user); - return $request->wantsJson() - ? response()->json(['two_factor' => true]) - : redirect()->route('two-factor.login'); + if ($user->getTable() == 'admins') { + + return $request->wantsJson() + ? response()->json(['admin.two_factor' => true]) + : redirect()->route('admin.two-factor.login'); + } else { + return $request->wantsJson() + ? response()->json(['two_factor' => true]) + : redirect()->route('two-factor.login'); + } + } } diff --git a/app/Actions/Jetstream/DeleteUser.php b/app/Actions/Jetstream/DeleteUser.php index 65a127f..1031ac9 100644 --- a/app/Actions/Jetstream/DeleteUser.php +++ b/app/Actions/Jetstream/DeleteUser.php @@ -2,6 +2,7 @@ namespace App\Actions\Jetstream; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Laravel\Jetstream\Contracts\DeletesTeams; use Laravel\Jetstream\Contracts\DeletesUsers; @@ -50,10 +51,12 @@ public function delete($user) */ protected function deleteTeams($user) { - $user->teams()->detach(); + if(!Auth::guard('admin')->check()) { + $user->teams()->detach(); - $user->ownedTeams->each(function ($team) { - $this->deletesTeams->delete($team); - }); + $user->ownedTeams->each(function ($team) { + $this->deletesTeams->delete($team); + }); + } } } diff --git a/app/Http/Livewire/AdminTwoFactorAuthenticationForm.php b/app/Http/Livewire/AdminTwoFactorAuthenticationForm.php new file mode 100644 index 0000000..287b9c7 --- /dev/null +++ b/app/Http/Livewire/AdminTwoFactorAuthenticationForm.php @@ -0,0 +1,181 @@ +two_factor_confirmed_at)) { + app(DisableTwoFactorAuthentication::class)(Auth::user()); + } + } + + /** + * Enable two factor authentication for the user. + * + * @param \Laravel\Fortify\Actions\EnableTwoFactorAuthentication $enable + * @return void + */ + public function enableTwoFactorAuthentication(EnableTwoFactorAuthentication $enable) + { + + if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) { + $this->ensurePasswordIsConfirmed(); + } + + $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; + } + + /** + * Display the user's recovery codes. + * + * @return void + */ + public function showRecoveryCodes() + { + if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) { + $this->ensurePasswordIsConfirmed(); + } + + $this->showingRecoveryCodes = true; + } + + /** + * Generate new recovery codes for the user. + * + * @param \Laravel\Fortify\Actions\GenerateNewRecoveryCodes $generate + * @return void + */ + public function regenerateRecoveryCodes(GenerateNewRecoveryCodes $generate) + { + if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) { + $this->ensurePasswordIsConfirmed(); + } + + $generate(Auth::user()); + + $this->showingRecoveryCodes = true; + } + + /** + * Disable two factor authentication for the user. + * + * @param \Laravel\Fortify\Actions\DisableTwoFactorAuthentication $disable + * @return void + */ + public function disableTwoFactorAuthentication(DisableTwoFactorAuthentication $disable) + { + if (Features::optionEnabled(Features::twoFactorAuthentication(), 'confirmPassword')) { + $this->ensurePasswordIsConfirmed(); + } + + $disable(Auth::user()); + + $this->showingQrCode = false; + $this->showingConfirmation = false; + $this->showingRecoveryCodes = false; + } + + /** + * Get the current user of the application. + * + * @return mixed + */ + public function getUserProperty() + { + return Auth::user(); + } + + /** + * Determine if two factor authentication is enabled. + * + * @return bool + */ + public function getEnabledProperty() + { + return ! empty($this->user->two_factor_secret); + } + + /** + * Render the component. + * + * @return \Illuminate\View\View + */ + public function render() + { + return view('profile.admin-two-factor-authentication-form'); + } +} diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 3ff764a..1d1bf36 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -1,7 +1,9 @@ app - ->when([AdminController::class, AttemptToAuthenticate::class, RedirectIfTwoFactorAuthenticatable::class, - PasswordResetLinkController::class, NewPasswordController::class,ProfileInformationController::class, UpdateUserProfileInformation::class]) + ->when([AdminController::class, AttemptToAuthenticate::class, RedirectIfTwoFactorAuthenticatable::class,ConfirmPassword::class, + PasswordResetLinkController::class, NewPasswordController::class,ProfileInformationController::class, UpdateUserProfileInformation::class,TwoFactorAuthenticatedSessionController::class]) ->needs(StatefulGuard::class) ->give(function () { return Auth::guard('admin'); diff --git a/app/Traits/ConfirmsPasswords.php b/app/Traits/ConfirmsPasswords.php new file mode 100644 index 0000000..90ecca9 --- /dev/null +++ b/app/Traits/ConfirmsPasswords.php @@ -0,0 +1,116 @@ +resetErrorBag(); + + if ($this->passwordIsConfirmed()) { + return $this->dispatchBrowserEvent('password-confirmed', [ + 'id' => $confirmableId, + ]); + } + + $this->confirmingPassword = true; + $this->confirmableId = $confirmableId; + $this->confirmablePassword = ''; + + $this->dispatchBrowserEvent('confirming-password'); + } + + /** + * Stop confirming the user's password. + * + * @return void + */ + public function stopConfirmingPassword() + { + $this->confirmingPassword = false; + $this->confirmableId = null; + $this->confirmablePassword = ''; + } + + /** + * Confirm the user's password. + * + * @return void + */ + public function confirmPassword() + { + if (! app(ConfirmPassword::class)(app(StatefulGuard::class), Auth::user(), $this->confirmablePassword)) { + throw ValidationException::withMessages([ + 'confirmable_password' => [__('This password does not match our records.')], + ]); + } + + session(['auth.password_confirmed_at' => time()]); + + $this->dispatchBrowserEvent('password-confirmed', [ + 'id' => $this->confirmableId, + ]); + + $this->stopConfirmingPassword(); + } + + /** + * Ensure that the user's password has been recently confirmed. + * + * @param int|null $maximumSecondsSinceConfirmation + * @return void + */ + protected function ensurePasswordIsConfirmed($maximumSecondsSinceConfirmation = null) + { + $maximumSecondsSinceConfirmation = $maximumSecondsSinceConfirmation ?: config('auth.password_timeout', 900); + + $this->passwordIsConfirmed($maximumSecondsSinceConfirmation) ? null : abort(403); + } + + /** + * Determine if the user's password has been recently confirmed. + * + * @param int|null $maximumSecondsSinceConfirmation + * @return bool + */ + protected function passwordIsConfirmed($maximumSecondsSinceConfirmation = null) + { + $maximumSecondsSinceConfirmation = $maximumSecondsSinceConfirmation ?: config('auth.password_timeout', 900); + + return (time() - session('auth.password_confirmed_at', 0)) < $maximumSecondsSinceConfirmation; + } +} diff --git a/database/migrations/2014_10_12_300000_add_two_factor_columns_to_admins_table.php b/database/migrations/2014_10_12_300000_add_two_factor_columns_to_admins_table.php new file mode 100644 index 0000000..5f9e4f8 --- /dev/null +++ b/database/migrations/2014_10_12_300000_add_two_factor_columns_to_admins_table.php @@ -0,0 +1,50 @@ +text('two_factor_secret') + ->after('password') + ->nullable(); + + $table->text('two_factor_recovery_codes') + ->after('two_factor_secret') + ->nullable(); + + if (Fortify::confirmsTwoFactorAuthentication()) { + $table->timestamp('two_factor_confirmed_at') + ->after('two_factor_recovery_codes') + ->nullable(); + } + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('admins', function (Blueprint $table) { + $table->dropColumn(array_merge([ + 'two_factor_secret', + 'two_factor_recovery_codes', + ], Fortify::confirmsTwoFactorAuthentication() ? [ + 'two_factor_confirmed_at', + ] : [])); + }); + } +}; diff --git a/resources/views/navigation-menu.blade.php b/resources/views/navigation-menu.blade.php index 96bf7a5..fcfab70 100644 --- a/resources/views/navigation-menu.blade.php +++ b/resources/views/navigation-menu.blade.php @@ -10,7 +10,7 @@ diff --git a/resources/views/profile/admin-two-factor-authentication-form.blade.php b/resources/views/profile/admin-two-factor-authentication-form.blade.php new file mode 100644 index 0000000..de42e4f --- /dev/null +++ b/resources/views/profile/admin-two-factor-authentication-form.blade.php @@ -0,0 +1,126 @@ + + + {{ __('Two Factor Authentication') }} + + + + {{ __('Add additional security to your account using two factor authentication.') }} + + + +

+ @if ($this->enabled) + @if ($showingConfirmation) + {{ __('Finish enabling two factor authentication.') }} + @else + {{ __('You have enabled two factor authentication.') }} + @endif + @else + {{ __('You have not enabled two factor authentication.') }} + @endif +

+ +
+

+ {{ __('When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone\'s Google Authenticator application.') }} +

+
+ + @if ($this->enabled) + @if ($showingQrCode) +
+

+ @if ($showingConfirmation) + {{ __('To finish enabling two factor authentication, scan the following QR code using your phone\'s authenticator application or enter the setup key and provide the generated OTP code.') }} + @else + {{ __('Two factor authentication is now enabled. Scan the following QR code using your phone\'s authenticator application or enter the setup key.') }} + @endif +

+
+ +
+ + {!! $this->user->twoFactorQrCodeSvg() !!} + +
+ +
+

+ {{ __('Setup Key') }}: {{ decrypt($this->user->two_factor_secret) }} +

+
+ + @if ($showingConfirmation) +
+ + + + + +
+ @endif + @endif + + @if ($showingRecoveryCodes) +
+

+ {{ __('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.') }} +

+
+ +
+ @foreach (json_decode(decrypt($this->user->two_factor_recovery_codes), true) as $code) +
{{ $code }}
+ @endforeach +
+ @endif + @endif + +
+ @if (! $this->enabled) + + + {{ __('Enable') }} + + + @else + @if ($showingRecoveryCodes) + + + {{ __('Regenerate Recovery Codes') }} + + + @elseif ($showingConfirmation) + + + {{ __('Confirm') }} + + + @else + + + {{ __('Show Recovery Codes') }} + + + @endif + + @if ($showingConfirmation) + + + {{ __('Cancel') }} + + + @else + + + {{ __('Disable') }} + + + @endif + + @endif +
+
+
diff --git a/resources/views/profile/show.blade.php b/resources/views/profile/show.blade.php index ea077b5..5d28d28 100644 --- a/resources/views/profile/show.blade.php +++ b/resources/views/profile/show.blade.php @@ -16,33 +16,39 @@ @endif - {{-- @if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::updatePasswords()))--}} - {{--
--}} - {{-- @livewire('profile.update-password-form')--}} - {{--
--}} + @if (Laravel\Fortify\Features::enabled(Laravel\Fortify\Features::updatePasswords())) +
+ @livewire('profile.update-password-form') +
- {{-- --}} - {{-- @endif--}} + + @endif - {{-- @if (Laravel\Fortify\Features::canManageTwoFactorAuthentication())--}} - {{--
--}} - {{-- @livewire('profile.two-factor-authentication-form')--}} - {{--
--}} + @if (Laravel\Fortify\Features::canManageTwoFactorAuthentication()) +
- {{-- --}} - {{-- @endif--}} + @if(Auth::guard('admin')->check()) + @livewire('admin-two-factor-authentication-form') + @else + @livewire('profile.two-factor-authentication-form') + @endif - {{--
--}} - {{-- @livewire('profile.logout-other-browser-sessions-form')--}} - {{--
--}} +
- {{-- @if (Laravel\Jetstream\Jetstream::hasAccountDeletionFeatures())--}} - {{-- --}} + + @endif - {{--
--}} - {{-- @livewire('profile.delete-user-form')--}} - {{--
--}} - {{-- @endif--}} +
+ @livewire('profile.logout-other-browser-sessions-form') +
+ + @if (Laravel\Jetstream\Jetstream::hasAccountDeletionFeatures()) + + +
+ @livewire('profile.delete-user-form') +
+ @endif diff --git a/resources/views/profile/two-factor-authentication-form.blade.php b/resources/views/profile/two-factor-authentication-form.blade.php index 4af049a..92e2a16 100644 --- a/resources/views/profile/two-factor-authentication-form.blade.php +++ b/resources/views/profile/two-factor-authentication-form.blade.php @@ -38,8 +38,10 @@

-
+
+ {!! $this->user->twoFactorQrCodeSvg() !!} +
diff --git a/resources/views/vendor/jetstream/components/application-mark.blade.php b/resources/views/vendor/jetstream/components/application-mark.blade.php index 55c25f2..c96d233 100644 --- a/resources/views/vendor/jetstream/components/application-mark.blade.php +++ b/resources/views/vendor/jetstream/components/application-mark.blade.php @@ -1,4 +1,8 @@ - - - + + + diff --git a/routes/admin.php b/routes/admin.php index 115a3e3..6d029de 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -1,6 +1,12 @@ name('profile.show'); Route::put('/profile-information', [\App\Actions\Fortify\Controllers\ProfileInformationController::class, 'update'])->name('user-profile-information.update'); + Route::get('/two-factor-challenge', [TwoFactorAuthenticatedSessionController::class, 'create'])->name('two-factor.login'); + Route::post('/admin/two-factor-authentication', [TwoFactorAuthenticationController::class, 'store'])->name('two-factor.enable'); + + Route::post('/admin/confirmed-two-factor-authentication', [ConfirmedTwoFactorAuthenticationController::class, 'store'])->name('two-factor.confirm'); + + Route::delete('/admin/two-factor-authentication', [TwoFactorAuthenticationController::class, 'destroy']) + ->name('two-factor.disable'); + + Route::get('/admin/two-factor-qr-code', [TwoFactorQrCodeController::class, 'show']) + ->name('two-factor.qr-code'); + + Route::get('/admin/two-factor-secret-key', [TwoFactorSecretKeyController::class, 'show']) + ->name('two-factor.secret-key'); + + Route::get('/admin/two-factor-recovery-codes', [RecoveryCodeController::class, 'index']) + ->name('two-factor.recovery-codes'); + + Route::post('/admin/two-factor-recovery-codes', [RecoveryCodeController::class, 'store']); + Route::post('logout', [\App\Http\Controllers\AdminController::class, 'destroy'])->name('logout'); }); });