Two factor (#47)

* init 2fa

* fix code style
This commit is contained in:
Saeed Vaziry 2023-09-11 20:47:44 +02:00 committed by GitHub
parent 13d4529d42
commit 9030427f78
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 652 additions and 259 deletions

View File

@ -0,0 +1,18 @@
<?php
namespace App\Actions\User;
use Laravel\Fortify\Rules\Password;
trait PasswordValidationRules
{
/**
* Get the validation rules used to validate passwords.
*
* @return array<int, \Illuminate\Contracts\Validation\Rule|array|string>
*/
protected function passwordRules(): array
{
return ['required', 'string', new Password, 'confirmed'];
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Actions\User;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Laravel\Fortify\Contracts\ResetsUserPasswords;
class ResetUserPassword implements ResetsUserPasswords
{
use PasswordValidationRules;
/**
* Validate and reset the user's forgotten password.
*
* @param array<string, string> $input
*/
public function reset(User $user, array $input): void
{
Validator::make($input, [
'password' => $this->passwordRules(),
])->validate();
$user->forceFill([
'password' => Hash::make($input['password']),
])->save();
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\Auth\LoginRequest;
use App\Providers\RouteServiceProvider;
use Illuminate\Contracts\View\View;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthenticatedSessionController extends Controller
{
/**
* Display the login view.
*/
public function create(): View
{
return view('auth.login');
}
/**
* Handle an incoming authentication request.
*/
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(RouteServiceProvider::HOME);
}
/**
* Destroy an authenticated session.
*/
public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login');
}
}

View File

@ -1,41 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
use Illuminate\View\View;
class ConfirmablePasswordController extends Controller
{
/**
* Show the confirm password view.
*/
public function show(): View
{
return view('auth.confirm-password');
}
/**
* Confirm the user's password.
*/
public function store(Request $request): RedirectResponse
{
if (! Auth::guard('web')->validate([
'email' => $request->user()->email,
'password' => $request->password,
])) {
throw ValidationException::withMessages([
'password' => __('auth.password'),
]);
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(RouteServiceProvider::HOME);
}
}

View File

@ -1,61 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class NewPasswordController extends Controller
{
/**
* Display the password reset view.
*/
public function create(Request $request): View
{
return view('auth.reset-password', ['request' => $request]);
}
/**
* Handle an incoming new password request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user) use ($request) {
$user->forceFill([
'password' => Hash::make($request->password),
'remember_token' => Str::random(60),
])->save();
event(new PasswordReset($user));
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $status == Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@ -1,29 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules\Password;
class PasswordController extends Controller
{
/**
* Update the user's password.
*/
public function update(Request $request): RedirectResponse
{
$validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
}
}

View File

@ -1,44 +0,0 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\View\View;
class PasswordResetLinkController extends Controller
{
/**
* Display the password reset link request view.
*/
public function create(): View
{
return view('auth.forgot-password');
}
/**
* Handle an incoming password reset link request.
*
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request): RedirectResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::sendResetLink(
$request->only('email')
);
return $status == Password::RESET_LINK_SENT
? back()->with('status', __($status))
: back()->withInput($request->only('email'))
->withErrors(['email' => __($status)]);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace App\Http\Livewire\Profile;
use Illuminate\Contracts\View\View;
use Livewire\Component;
class TwoFactorAuthentication extends Component
{
public function render(): View
{
return view('livewire.profile.two-factor-authentication');
}
}

View File

@ -4,6 +4,7 @@
use App\Actions\User\UpdateUserProfileInformation; use App\Actions\User\UpdateUserProfileInformation;
use App\Http\Livewire\UserDropdown; use App\Http\Livewire\UserDropdown;
use App\Models\User;
use Exception; use Exception;
use Illuminate\Contracts\View\View; use Illuminate\Contracts\View\View;
use Livewire\Component; use Livewire\Component;
@ -34,8 +35,10 @@ public function submit(): void
public function sendVerificationEmail(): void public function sendVerificationEmail(): void
{ {
if (! auth()->user()->hasVerifiedEmail()) { /** @var User $user */
auth()->user()->sendEmailVerificationNotification(); $user = auth()->user();
if (! $user->hasVerifiedEmail()) {
$user->sendEmailVerificationNotification();
session()->flash('status', 'verification-link-sent'); session()->flash('status', 'verification-link-sent');
} }

View File

@ -8,6 +8,7 @@
use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens; use Laravel\Sanctum\HasApiTokens;
/** /**
@ -33,6 +34,7 @@ class User extends Authenticatable
use HasApiTokens; use HasApiTokens;
use HasFactory; use HasFactory;
use Notifiable; use Notifiable;
use TwoFactorAuthenticatable;
protected $fillable = [ protected $fillable = [
'name', 'name',

View File

@ -0,0 +1,63 @@
<?php
namespace App\Providers;
use App\Actions\User\ResetUserPassword;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;
use Laravel\Fortify\Fortify;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
return Limit::perMinute(5)->by($throttleKey);
});
RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
Fortify::loginView(function () {
return view('auth.login');
});
Fortify::requestPasswordResetLinkView(function () {
return view('auth.forgot-password');
});
Fortify::resetPasswordView(function (Request $request) {
return view('auth.reset-password', [
'token' => $request->route('token'),
'email' => $request->query('email'),
]);
});
Fortify::confirmPasswordView(function () {
return view('auth.confirm-password');
});
Fortify::twoFactorChallengeView(function () {
return view('auth.two-factor-challenge');
});
}
}

View File

@ -9,6 +9,7 @@
"aws/aws-sdk-php": "^3.158", "aws/aws-sdk-php": "^3.158",
"bensampo/laravel-enum": "^6.3", "bensampo/laravel-enum": "^6.3",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"laravel/fortify": "^1.17",
"laravel/framework": "^10.0", "laravel/framework": "^10.0",
"laravel/sanctum": "^3.2", "laravel/sanctum": "^3.2",
"laravel/socialite": "^5.2", "laravel/socialite": "^5.2",

222
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "289f9ab3a3999301701254ea3181abe3", "content-hash": "0f725da8271d545b318e319f7b616053",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@ -154,6 +154,60 @@
}, },
"time": "2023-04-14T18:22:01+00:00" "time": "2023-04-14T18:22:01+00:00"
}, },
{
"name": "bacon/bacon-qr-code",
"version": "2.0.8",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22",
"reference": "8674e51bb65af933a5ffaf1c308a660387c35c22",
"shasum": ""
},
"require": {
"dasprid/enum": "^1.0.3",
"ext-iconv": "*",
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phly/keep-a-changelog": "^2.1",
"phpunit/phpunit": "^7 | ^8 | ^9",
"spatie/phpunit-snapshot-assertions": "^4.2.9",
"squizlabs/php_codesniffer": "^3.4"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"support": {
"issues": "https://github.com/Bacon/BaconQrCode/issues",
"source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8"
},
"time": "2022-12-07T17:46:57+00:00"
},
{ {
"name": "bensampo/laravel-enum", "name": "bensampo/laravel-enum",
"version": "v6.3.1", "version": "v6.3.1",
@ -437,6 +491,56 @@
], ],
"time": "2022-11-17T09:50:14+00:00" "time": "2022-11-17T09:50:14+00:00"
}, },
{
"name": "dasprid/enum",
"version": "1.0.5",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
"reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/6faf451159fb8ba4126b925ed2d78acfce0dc016",
"reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016",
"shasum": ""
},
"require": {
"php": ">=7.1 <9.0"
},
"require-dev": {
"phpunit/phpunit": "^7 | ^8 | ^9",
"squizlabs/php_codesniffer": "*"
},
"type": "library",
"autoload": {
"psr-4": {
"DASPRiD\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/",
"role": "Developer"
}
],
"description": "PHP 7.1 enum implementation",
"keywords": [
"enum",
"map"
],
"support": {
"issues": "https://github.com/DASPRiD/Enum/issues",
"source": "https://github.com/DASPRiD/Enum/tree/1.0.5"
},
"time": "2023-08-25T16:18:39+00:00"
},
{ {
"name": "dflydev/dot-access-data", "name": "dflydev/dot-access-data",
"version": "v3.0.2", "version": "v3.0.2",
@ -1416,6 +1520,70 @@
], ],
"time": "2023-03-08T11:55:01+00:00" "time": "2023-03-08T11:55:01+00:00"
}, },
{
"name": "laravel/fortify",
"version": "v1.17.6",
"source": {
"type": "git",
"url": "https://github.com/laravel/fortify.git",
"reference": "dd1785363ef213e8d71be7949f24f7317b95e238"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/fortify/zipball/dd1785363ef213e8d71be7949f24f7317b95e238",
"reference": "dd1785363ef213e8d71be7949f24f7317b95e238",
"shasum": ""
},
"require": {
"bacon/bacon-qr-code": "^2.0",
"ext-json": "*",
"illuminate/support": "^8.82|^9.0|^10.0",
"php": "^7.3|^8.0",
"pragmarx/google2fa": "^7.0|^8.0"
},
"require-dev": {
"mockery/mockery": "^1.0",
"orchestra/testbench": "^6.0|^7.0|^8.0",
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
},
"laravel": {
"providers": [
"Laravel\\Fortify\\FortifyServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Laravel\\Fortify\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"description": "Backend controllers and scaffolding for Laravel authentication.",
"keywords": [
"auth",
"laravel"
],
"support": {
"issues": "https://github.com/laravel/fortify/issues",
"source": "https://github.com/laravel/fortify"
},
"time": "2023-09-04T16:25:25+00:00"
},
{ {
"name": "laravel/framework", "name": "laravel/framework",
"version": "v10.7.1", "version": "v10.7.1",
@ -3301,6 +3469,58 @@
], ],
"time": "2023-07-09T15:24:48+00:00" "time": "2023-07-09T15:24:48+00:00"
}, },
{
"name": "pragmarx/google2fa",
"version": "v8.0.1",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/google2fa.git",
"reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/80c3d801b31fe165f8fe99ea085e0a37834e1be3",
"reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3",
"shasum": ""
},
"require": {
"paragonie/constant_time_encoding": "^1.0|^2.0",
"php": "^7.1|^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.18",
"phpunit/phpunit": "^7.5.15|^8.5|^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"PragmaRX\\Google2FA\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"email": "acr@antoniocarlosribeiro.com",
"role": "Creator & Designer"
}
],
"description": "A One Time Password Authentication package, compatible with Google Authenticator.",
"keywords": [
"2fa",
"Authentication",
"Two Factor Authentication",
"google2fa"
],
"support": {
"issues": "https://github.com/antonioribeiro/google2fa/issues",
"source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.1"
},
"time": "2022-06-13T21:57:56+00:00"
},
{ {
"name": "psr/container", "name": "psr/container",
"version": "2.0.2", "version": "2.0.2",

View File

@ -185,6 +185,7 @@
/* /*
* Package Service Providers... * Package Service Providers...
*/ */
App\Providers\FortifyServiceProvider::class,
/* /*
* Application Service Providers... * Application Service Providers...

147
config/fortify.php Normal file
View File

@ -0,0 +1,147 @@
<?php
use App\Providers\RouteServiceProvider;
use Laravel\Fortify\Features;
return [
/*
|--------------------------------------------------------------------------
| Fortify Guard
|--------------------------------------------------------------------------
|
| Here you may specify which authentication guard Fortify will use while
| authenticating users. This value should correspond with one of your
| guards that is already present in your "auth" configuration file.
|
*/
'guard' => 'web',
/*
|--------------------------------------------------------------------------
| Fortify Password Broker
|--------------------------------------------------------------------------
|
| Here you may specify which password broker Fortify can use when a user
| is resetting their password. This configured value should match one
| of your password brokers setup in your "auth" configuration file.
|
*/
'passwords' => 'users',
/*
|--------------------------------------------------------------------------
| Username / Email
|--------------------------------------------------------------------------
|
| This value defines which model attribute should be considered as your
| application's "username" field. Typically, this might be the email
| address of the users but you are free to change this value here.
|
| Out of the box, Fortify expects forgot password and reset password
| requests to have a field named 'email'. If the application uses
| another name for the field you may define it below as needed.
|
*/
'username' => 'email',
'email' => 'email',
/*
|--------------------------------------------------------------------------
| Home Path
|--------------------------------------------------------------------------
|
| Here you may configure the path where users will get redirected during
| authentication or password reset when the operations are successful
| and the user is authenticated. You are free to change this value.
|
*/
'home' => RouteServiceProvider::HOME,
/*
|--------------------------------------------------------------------------
| Fortify Routes Prefix / Subdomain
|--------------------------------------------------------------------------
|
| Here you may specify which prefix Fortify will assign to all the routes
| that it registers with the application. If necessary, you may change
| subdomain under which all of the Fortify routes will be available.
|
*/
'prefix' => '',
'domain' => null,
/*
|--------------------------------------------------------------------------
| Fortify Routes Middleware
|--------------------------------------------------------------------------
|
| Here you may specify which middleware Fortify will assign to the routes
| that it registers with the application. If necessary, you may change
| these middleware but typically this provided default is preferred.
|
*/
'middleware' => ['web'],
/*
|--------------------------------------------------------------------------
| Rate Limiting
|--------------------------------------------------------------------------
|
| By default, Fortify will throttle logins to five requests per minute for
| every email and IP address combination. However, if you would like to
| specify a custom rate limiter to call then you may specify it here.
|
*/
'limiters' => [
'login' => 'login',
'two-factor' => 'two-factor',
],
/*
|--------------------------------------------------------------------------
| Register View Routes
|--------------------------------------------------------------------------
|
| Here you may specify if the routes returning views should be disabled as
| you may not need them when building your own application. This may be
| especially true if you're writing a custom single-page application.
|
*/
'views' => true,
/*
|--------------------------------------------------------------------------
| Features
|--------------------------------------------------------------------------
|
| Some of the Fortify features are optional. You may disable the features
| by removing them from this array. You're free to only remove some of
| these features or you can even remove all of these if you need to.
|
*/
'features' => [
// Features::registration(),
Features::resetPasswords(),
// Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
Features::twoFactorAuthentication([
// 'confirm' => true,
// 'confirmPassword' => true,
// 'window' => 0,
]),
],
];

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->timestamp('two_factor_confirmed_at')
->after('two_factor_recovery_codes')
->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn([
'two_factor_confirmed_at',
]);
});
}
};

View File

@ -1,14 +1,14 @@
<x-guest-layout> <x-guest-layout>
<form method="POST" action="{{ route('password.store') }}"> <form method="POST" action="{{ route('password.update') }}">
@csrf @csrf
<!-- Password Reset Token --> <!-- Password Reset Token -->
<input type="hidden" name="token" value="{{ $request->route('token') }}"> <input type="hidden" name="token" value="{{ $token }}">
<!-- Email Address --> <!-- Email Address -->
<div> <div>
<x-input-label for="email" :value="__('Email')" /> <x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $request->email)" required autofocus autocomplete="username" /> <x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email', $email)" required autofocus autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" /> <x-input-error :messages="$errors->get('email')" class="mt-2" />
</div> </div>

View File

@ -0,0 +1,50 @@
<x-guest-layout>
<div x-data="{recover: @if($errors->has('recovery_code')) true @else false @endif}">
<div x-show="recover">
<form method="POST">
@csrf
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{{ __('Please enter your recovery code') }}
</div>
<div>
<x-input-label for="recovery_code" :value="__('Recovery Code')" />
<x-text-input id="recovery_code" class="block mt-1 w-full" type="text" name="recovery_code" required autofocus autocomplete="recovery_code" />
<x-input-error :messages="$errors->get('recovery_code')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-secondary-button class="mr-2" x-on:click="recover = false">
{{ __('Login') }}
</x-secondary-button>
<x-primary-button type="submit">
{{ __('Recover') }}
</x-primary-button>
</div>
</form>
</div>
<div x-show="!recover">
<form method="POST">
@csrf
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
{{ __('Please confirm access to your account by entering the authentication code provided by your authenticator application.') }}
</div>
<div>
<x-input-label for="code" :value="__('Code')" />
<x-text-input id="code" class="block mt-1 w-full" type="text" name="code" required autofocus autocomplete="code" />
<x-input-error :messages="$errors->get('code')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-secondary-button class="mr-2" x-on:click="recover = true">
{{ __('Recover') }}
</x-secondary-button>
<x-primary-button type="submit">
{{ __('Login') }}
</x-primary-button>
</div>
</form>
</div>
</div>
</x-guest-layout>

View File

@ -0,0 +1,60 @@
<x-card>
<x-slot name="title">
{{ __('Two Factor Authentication') }}
</x-slot>
<x-slot name="description">
{{ __('Here you can activate 2FA to secure your account') }}
</x-slot>
@if(! auth()->user()->two_factor_secret)
{{-- Enable 2FA --}}
<form method="POST" action="{{ route('two-factor.enable') }}">
@csrf
<x-primary-button type="submit">
{{ __('Enable Two-Factor') }}
</x-primary-button>
</form>
@else
{{-- Disable 2FA --}}
<form method="POST" action="{{ route('two-factor.disable') }}">
@csrf
@method('DELETE')
<x-danger-button type="submit">
{{ __('Disable Two-Factor') }}
</x-danger-button>
</form>
@if(session('status') == 'two-factor-authentication-enabled')
<div class="mt-5">
{{ __('Two factor authentication is now enabled. Scan the following QR code using your phone\'s authenticator application.') }}
</div>
<div class="mt-5">
{!! auth()->user()->twoFactorQrCodeSvg() !!}
</div>
@endif
{{-- Show 2FA Recovery Codes --}}
<div class="mt-5">
{{ __('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.') }}
</div>
<div class="mt-5 p-2 rounded-md border border-gray-100 dark:border-gray-700">
@foreach (json_decode(decrypt(auth()->user()->two_factor_recovery_codes), true) as $code)
<div class="mt-2">{{ $code }}</div>
@endforeach
</div>
{{-- Regenerate 2FA Recovery Codes --}}
<form class="mt-5" method="POST" action="{{ route('two-factor.recovery-codes') }}">
@csrf
<x-primary-button type="submit">
{{ __('Regenerate Recovery Codes') }}
</x-primary-button>
</form>
@endif
</x-card>

View File

@ -4,4 +4,6 @@
<livewire:profile.update-profile-information /> <livewire:profile.update-profile-information />
<livewire:profile.update-password /> <livewire:profile.update-password />
<livewire:profile.two-factor-authentication />
</x-profile-layout> </x-profile-layout>

View File

@ -1,24 +0,0 @@
<?php
use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\ConfirmablePasswordController;
use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use Illuminate\Support\Facades\Route;
Route::middleware('guest')->group(function () {
Route::get('login', [AuthenticatedSessionController::class, 'create'])->name('login');
Route::post('login', [AuthenticatedSessionController::class, 'store']);
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])->name('password.request');
Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])->name('password.email');
Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])->name('password.reset');
Route::post('reset-password', [NewPasswordController::class, 'store'])->name('password.store');
});
Route::middleware('auth')->group(function () {
Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])->name('password.confirm');
Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);
Route::put('password', [PasswordController::class, 'update'])->name('password.update');
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])->name('logout');
});

View File

@ -54,5 +54,3 @@
}); });
}); });
}); });
require __DIR__.'/auth.php';

View File

@ -13,7 +13,7 @@ public function test_logout(): void
{ {
$this->actingAs($this->user); $this->actingAs($this->user);
$this->post(route('logout'))->assertRedirect(route('login')); $this->post(route('logout'))->assertRedirect('/');
$this->assertFalse(auth()->check()); $this->assertFalse(auth()->check());
} }

View File

@ -14,7 +14,7 @@ public function test_confirm_password_screen_can_be_rendered(): void
{ {
$this->actingAs($this->user); $this->actingAs($this->user);
$response = $this->get('/confirm-password'); $response = $this->get(route('password.confirm'));
$response->assertStatus(200); $response->assertStatus(200);
} }
@ -26,7 +26,7 @@ public function test_password_can_be_confirmed(): void
{ {
$this->actingAs($this->user); $this->actingAs($this->user);
$response = $this->post('/confirm-password', [ $response = $this->post(route('password.confirm'), [
'password' => 'password', 'password' => 'password',
]); ]);
@ -38,7 +38,7 @@ public function test_password_is_not_confirmed_with_invalid_password(): void
{ {
$this->actingAs($this->user); $this->actingAs($this->user);
$response = $this->post('/confirm-password', [ $response = $this->post(route('password.confirm'), [
'password' => 'wrong-password', 'password' => 'wrong-password',
]); ]);