diff --git a/database/migrations/2024_05_14_091321_create_auth_codes_table.php b/database/migrations/2024_05_14_091321_create_auth_codes_table.php new file mode 100644 index 00000000..703468eb --- /dev/null +++ b/database/migrations/2024_05_14_091321_create_auth_codes_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->integer('code')->unsigned(); + $table->timestamp('expires_at'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('root_auth_codes'); + } +}; diff --git a/resources/views/auth/two-factor.blade.php b/resources/views/auth/two-factor.blade.php index ad731ff9..61deec33 100644 --- a/resources/views/auth/two-factor.blade.php +++ b/resources/views/auth/two-factor.blade.php @@ -5,18 +5,41 @@ {{-- Content --}} @section('content') -
{{ __('To finish the two factor authentication, please use the link we sent, or request a new one!') }}
- - +@endsection + +{{-- Footer --}} +@section('footer') + + @endsection diff --git a/routes/auth.php b/routes/auth.php index e020b865..31f50f23 100644 --- a/routes/auth.php +++ b/routes/auth.php @@ -17,7 +17,7 @@ Route::get('/password/reset/{token}/{email}', [ResetPasswordController::class, 'show'])->name('password.reset'); Route::post('/password/reset', [ResetPasswordController::class, 'reset'])->name('password.update'); -// Verify -Route::get('/two-factor', [TwoFactorController::class, 'verify'])->name('two-factor.verify'); -Route::get('/two-factor/resend', [TwoFactorController::class, 'show'])->name('two-factor.show'); +// Two Factor Verification +Route::get('/two-factor', [TwoFactorController::class, 'show'])->name('two-factor.show'); +Route::post('/two-factor', [TwoFactorController::class, 'verify'])->name('two-factor.verify'); Route::post('/two-factor/resend', [TwoFactorController::class, 'resend'])->name('two-factor.resend'); diff --git a/src/Http/Controllers/Auth/LoginController.php b/src/Http/Controllers/Auth/LoginController.php index 36570b4d..b31f942f 100644 --- a/src/Http/Controllers/Auth/LoginController.php +++ b/src/Http/Controllers/Auth/LoginController.php @@ -3,8 +3,7 @@ namespace Cone\Root\Http\Controllers\Auth; use Cone\Root\Http\Controllers\Controller; -use Cone\Root\Interfaces\TwoFactorAuthenticatable; -use Cone\Root\Notifications\TwoFactorLink; +use Cone\Root\Notifications\AuthCodeNotification; use Illuminate\Auth\Events\Login; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -62,11 +61,10 @@ public function login(Request $request): RedirectResponse new Login(Auth::getDefaultDriver(), $request->user(), $request->filled('remember')) ); - if ($request->user()->can('viewRoot') - && $request->user() instanceof TwoFactorAuthenticatable - && $request->user()->requiresTwoFactorAuthentication() - ) { - $request->user()->notify(new TwoFactorLink()); + if ($request->user()->can('viewRoot') && $request->user()->shouldTwoFactorAuthenticate($request)) { + $request->user()->notify( + new AuthCodeNotification($request->user()->generateAuthCode()) + ); $request->session()->flash('status', __('The two factor authentication link has been sent!')); } diff --git a/src/Http/Controllers/Auth/TwoFactorController.php b/src/Http/Controllers/Auth/TwoFactorController.php index fe4f235f..cce4f37c 100644 --- a/src/Http/Controllers/Auth/TwoFactorController.php +++ b/src/Http/Controllers/Auth/TwoFactorController.php @@ -5,11 +5,12 @@ use Closure; use Cone\Root\Http\Controllers\Controller; use Cone\Root\Http\Middleware\Authenticate; -use Cone\Root\Interfaces\TwoFactorAuthenticatable; -use Cone\Root\Notifications\TwoFactorLink; +use Cone\Root\Notifications\AuthCodeNotification; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Http\Response; +use Illuminate\Support\Facades\Cookie; +use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\Response as ResponseFactory; use Illuminate\Support\Facades\URL; use Symfony\Component\HttpFoundation\Response as BaseResponse; @@ -24,10 +25,7 @@ public function __construct() $this->middleware(Authenticate::class); $this->middleware('throttle:6,1')->only(['resend']); $this->middleware(static function (Request $request, Closure $next): BaseResponse { - if (! $request->user() instanceof TwoFactorAuthenticatable - || ! $request->user()->requiresTwoFactorAuthentication() - || $request->session()->has('root.auth.two-factor') - ) { + if (! $request->user()->shouldTwoFactorAuthenticate($request)) { return ResponseFactory::redirectToIntended(URL::route('root.dashboard')); } @@ -40,7 +38,9 @@ public function __construct() */ public function show(Request $request): Response|RedirectResponse { - return ResponseFactory::view('root::auth.two-factor'); + return ResponseFactory::view('root::auth.two-factor', [ + 'code' => $request->input('code'), + ]); } /** @@ -48,13 +48,27 @@ public function show(Request $request): Response|RedirectResponse */ public function verify(Request $request): RedirectResponse { - if (! $request->hasValidSignature() || ! hash_equals($request->input('hash'), sha1($request->user()->email))) { + $data = $request->validate([ + 'code' => ['required', 'numeric'], + ]); + + if ($request->user()->authCode?->code !== (int) $data['code']) { return ResponseFactory::redirectToRoute('root.auth.two-factor.show') - ->with('status', __('The authentication link is not valid! Please request a new link!')); + ->withErrors(['code' => __('The authentication code is not valid!')]); } $request->session()->put('root.auth.two-factor', true); + $request->user()->authCodes()->delete(); + + if ($request->boolean('trust')) { + Cookie::queue( + 'device_token', + sha1(sprintf('%s:%s', $request->user()->getKey(), $request->user()->email)), + Date::now()->addYear()->diffInMinutes(absolute: true), + ); + } + return ResponseFactory::redirectToIntended(URL::route('root.dashboard')); } @@ -63,9 +77,11 @@ public function verify(Request $request): RedirectResponse */ public function resend(Request $request): RedirectResponse { - $request->user()->notify(new TwoFactorLink()); + $code = $request->user()->generateAuthCode(); + + $request->user()->notify(new AuthCodeNotification($code)); return ResponseFactory::redirectToRoute('root.auth.two-factor.show') - ->with('status', __('The two factor authentication link has been sent!')); + ->with('status', __('The authentication code has been sent!')); } } diff --git a/src/Http/Middleware/TwoFactorAuthenticate.php b/src/Http/Middleware/TwoFactorAuthenticate.php index 20c5232f..3bd2c026 100644 --- a/src/Http/Middleware/TwoFactorAuthenticate.php +++ b/src/Http/Middleware/TwoFactorAuthenticate.php @@ -3,7 +3,6 @@ namespace Cone\Root\Http\Middleware; use Closure; -use Cone\Root\Interfaces\TwoFactorAuthenticatable; use Illuminate\Http\Request; use Illuminate\Support\Facades\Redirect; use Symfony\Component\HttpFoundation\Response; @@ -17,10 +16,7 @@ class TwoFactorAuthenticate */ public function handle(Request $request, Closure $next): Response { - if ($request->user() instanceof TwoFactorAuthenticatable - && $request->user()->requiresTwoFactorAuthentication() - && ! $request->session()->has('root.auth.two-factor') - ) { + if ($request->user()->shouldTwoFactorAuthenticate($request)) { return Redirect::route('root.auth.two-factor.show'); } diff --git a/src/Interfaces/Models/AuthCode.php b/src/Interfaces/Models/AuthCode.php new file mode 100644 index 00000000..422a8dc6 --- /dev/null +++ b/src/Interfaces/Models/AuthCode.php @@ -0,0 +1,23 @@ + + */ + protected $casts = [ + 'code' => 'int', + 'expires_at' => 'datetime', + ]; + + /** + * The attributes that are mass assignable. + * + * @var array