From 3a3c84a8b96d080e465eaf4d622900ec3da2ece8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=2E=20Nagy=20Gerg=C5=91?= Date: Fri, 24 May 2024 11:23:46 +0200 Subject: [PATCH] wip --- ...4_05_14_091321_create_auth_codes_table.php | 30 +++++++ resources/views/auth/two-factor.blade.php | 42 +++++++-- routes/auth.php | 6 +- src/Http/Controllers/Auth/LoginController.php | 12 ++- .../Controllers/Auth/TwoFactorController.php | 38 +++++--- src/Http/Middleware/TwoFactorAuthenticate.php | 6 +- src/Interfaces/Models/AuthCode.php | 23 +++++ src/Interfaces/Models/User.php | 28 ++++++ src/Interfaces/TwoFactorAuthenticatable.php | 11 --- src/Models/AuthCode.php | 89 +++++++++++++++++++ src/Models/User.php | 68 ++++++++++++++ src/Notifications/AuthCodeNotification.php | 51 +++++++++++ src/Notifications/TwoFactorLink.php | 42 --------- src/RootServiceProvider.php | 1 + 14 files changed, 363 insertions(+), 84 deletions(-) create mode 100644 database/migrations/2024_05_14_091321_create_auth_codes_table.php create mode 100644 src/Interfaces/Models/AuthCode.php delete mode 100644 src/Interfaces/TwoFactorAuthenticatable.php create mode 100644 src/Models/AuthCode.php create mode 100644 src/Notifications/AuthCodeNotification.php delete mode 100644 src/Notifications/TwoFactorLink.php 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 000000000..703468eb0 --- /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 ad731ff9f..61deec33d 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!') }}

-
+

{{ __('To finish the two factor authentication, please add the verification code, or request a new one!') }}

+ @csrf
-
+
+ +
+
+
- +@endsection + +{{-- Footer --}} +@section('footer')
@csrf
@@ -25,4 +48,13 @@
+ +
+ @csrf +
+ +
+
@endsection diff --git a/routes/auth.php b/routes/auth.php index e020b865b..31f50f238 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 36570b4df..b31f942f8 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 fe4f235fa..cce4f37cb 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 20c5232fe..3bd2c026b 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 000000000..422a8dc65 --- /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 + */ + protected $fillable = []; + + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'root_auth_codes'; + + /** + * Get the proxied interface. + */ + public static function getProxiedInterface(): string + { + return Contract::class; + } + + /** + * Get the user for the model. + */ + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** + * Determine whether the code is active. + */ + public function active(): bool + { + return $this->expires_at->gt(Date::now()); + } + + /** + * Determine whether the code is expired. + */ + public function expired(): bool + { + return ! $this->active(); + } + + /** + * Scope the query only to include the active codes. + */ + public function scopeActive(Builder $query): Builder + { + return $query->where($query->qualifyColumn('expires_at'), '>', Date::now()); + } + + /** + * Scope the query only to include the expired codes. + */ + public function scopeExpired(Builder $query): Builder + { + return $query->where($query->qualifyColumn('expires_at'), '<=', Date::now()); + } +} diff --git a/src/Models/User.php b/src/Models/User.php index 1a3afdc83..9aaf0448d 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -8,9 +8,12 @@ use Cone\Root\Traits\InteractsWithProxy; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Foundation\Auth\User as Authenticatable; +use Illuminate\Http\Request; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Facades\Date; class User extends Authenticatable implements Contract { @@ -51,6 +54,71 @@ public function rootNotifications(): MorphMany return $this->morphMany(Notification::getProxiedClass(), 'notifiable')->latest(); } + /** + * Get the current auth code for the user. + */ + public function authCode(): HasOne + { + return $this->authCodes()->one()->ofMany()->active(); + } + + /** + * Get the auth codes for the user. + */ + public function authCodes(): HasMany + { + return $this->hasMany(AuthCode::getProxiedClass())->active(); + } + + /** + * Determine whether the object requires two factor authentitaction. + */ + public function requiresTwoFactorAuthentication(): bool + { + return false; + } + + /** + * Determine whether the user should be two factor authenticated. + */ + public function shouldTwoFactorAuthenticate(Request $request): bool + { + if (! $this->requiresTwoFactorAuthentication()) { + return false; + } + + if ($request->cookie('device_token') === $this->generateDeviceToken($request)) { + return false; + } + + return ! $request->session()->has('root.auth.two-factor'); + } + + /** + * Generate a new auth code for the user. + */ + public function generateAuthCode(): AuthCode + { + $this->authCodes()->delete(); + + $code = $this->authCodes()->make()->forceFill([ + 'code' => mt_rand(100000, 999999), + 'expires_at' => Date::now()->addMinutes(5), + ]); + + $code->save(); + + return $code; + } + + /** + * Generate a device token. + */ + public function generateDeviceToken(Request $request): string + { + return sha1(sprintf('%s:%s', $request->user()->getKey(), $request->user()->email)); + } + /** * Get the avatar attribute. * diff --git a/src/Notifications/AuthCodeNotification.php b/src/Notifications/AuthCodeNotification.php new file mode 100644 index 000000000..e5496b995 --- /dev/null +++ b/src/Notifications/AuthCodeNotification.php @@ -0,0 +1,51 @@ +code = $code; + } + + /** + * Get the notification's delivery channels. + * + * @return array + */ + public function via(object $notifiable): array + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + */ + public function toMail(object $notifiable): MailMessage + { + return (new MailMessage()) + ->subject(sprintf('%s - %s', Config::get('app.name'), __('Two Factor Code'))) + ->line(__('Your verification code is: :code.', ['code' => $this->code->code])) + ->action(__('Verify Authentication'), URL::route('root.auth.two-factor.show', ['code' => $this->code->code])) + ->line(__('The code expires at :date.', ['date' => $this->code->expires_at->format('Y-m-d H:i:s')])); + } +} diff --git a/src/Notifications/TwoFactorLink.php b/src/Notifications/TwoFactorLink.php deleted file mode 100644 index a48f4cd72..000000000 --- a/src/Notifications/TwoFactorLink.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ - public function via(object $notifiable): array - { - return ['mail']; - } - - /** - * Get the mail representation of the notification. - */ - public function toMail(object $notifiable): MailMessage - { - $url = URL::temporarySignedRoute( - 'root.auth.two-factor.verify', - Config::get('root.two_factor.expiration', 600), - ['hash' => sha1($notifiable->email)] - ); - - return (new MailMessage()) - ->subject(__('Two Factor Authentication Link')) - ->line(__('To finish the two factor authentication process, please click the link below.')) - ->action(__('Finish Two Factor Login'), $url); - } -} diff --git a/src/RootServiceProvider.php b/src/RootServiceProvider.php index c2bf8fa76..b58690b0b 100644 --- a/src/RootServiceProvider.php +++ b/src/RootServiceProvider.php @@ -32,6 +32,7 @@ class RootServiceProvider extends ServiceProvider * All of the container bindings that should be registered. */ public array $bindings = [ + Interfaces\Models\AuthCode::class => Models\AuthCode::class, Interfaces\Models\Medium::class => Models\Medium::class, Interfaces\Models\Meta::class => Models\Meta::class, Interfaces\Models\Notification::class => Models\Notification::class,