From 1624713b921cd4ececa5c33b7c93e499596e7af0 Mon Sep 17 00:00:00 2001 From: Amir Date: Tue, 1 Nov 2022 17:13:41 +0400 Subject: [PATCH] Implementation of Admin Two Factor Authentication Guard --- app/Actions/Fortify/AttemptToAuthenticate.php | 2 + ...woFactorAuthenticatedSessionController.php | 29 ++-- .../Fortify/PrepareAuthenticatedSession.php | 54 +++++++ .../RedirectIfTwoFactorAuthenticatable.php | 9 +- .../Requests/TwoFactorLoginRequest.php | 141 ++++++++++++++++++ app/Http/Controllers/AdminController.php | 11 +- app/Http/Controllers/UserController.php | 3 + app/Http/Kernel.php | 1 + .../AdminRedirectIfAuthenticated.php | 3 +- app/Http/Middleware/CheckGuard.php | 26 ++++ .../Middleware/RedirectIfAuthenticated.php | 2 +- app/Models/Admin.php | 4 + app/Models/User.php | 5 + app/Providers/FortifyServiceProvider.php | 41 ++++- app/Providers/RouteServiceProvider.php | 1 + .../views/auth/two-factor-challenge.blade.php | 47 ++++-- routes/admin.php | 8 +- routes/web.php | 10 ++ 18 files changed, 368 insertions(+), 29 deletions(-) create mode 100644 app/Actions/Fortify/PrepareAuthenticatedSession.php create mode 100644 app/Actions/Fortify/Requests/TwoFactorLoginRequest.php create mode 100644 app/Http/Middleware/CheckGuard.php diff --git a/app/Actions/Fortify/AttemptToAuthenticate.php b/app/Actions/Fortify/AttemptToAuthenticate.php index b6fdf6e..7d39475 100644 --- a/app/Actions/Fortify/AttemptToAuthenticate.php +++ b/app/Actions/Fortify/AttemptToAuthenticate.php @@ -4,6 +4,7 @@ use Illuminate\Auth\Events\Failed; use Illuminate\Contracts\Auth\StatefulGuard; +use Illuminate\Support\Facades\Session; use Illuminate\Validation\ValidationException; use Laravel\Fortify\Fortify; use Laravel\Fortify\LoginRateLimiter; @@ -35,6 +36,7 @@ public function __construct(StatefulGuard $guard, LoginRateLimiter $limiter) { $this->guard = $guard; $this->limiter = $limiter; + } /** diff --git a/app/Actions/Fortify/Controllers/TwoFactorAuthenticatedSessionController.php b/app/Actions/Fortify/Controllers/TwoFactorAuthenticatedSessionController.php index 0444c26..2228c82 100644 --- a/app/Actions/Fortify/Controllers/TwoFactorAuthenticatedSessionController.php +++ b/app/Actions/Fortify/Controllers/TwoFactorAuthenticatedSessionController.php @@ -2,14 +2,18 @@ namespace App\Actions\Fortify\Controllers; + use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Http\Exceptions\HttpResponseException; +use Illuminate\Http\Request; use Illuminate\Routing\Controller; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Session; use Laravel\Fortify\Contracts\FailedTwoFactorLoginResponse; use Laravel\Fortify\Contracts\TwoFactorChallengeViewResponse; use Laravel\Fortify\Contracts\TwoFactorLoginResponse; use Laravel\Fortify\Events\RecoveryCodeReplaced; -use Laravel\Fortify\Http\Requests\TwoFactorLoginRequest; +use App\Actions\Fortify\Requests\TwoFactorLoginRequest; class TwoFactorAuthenticatedSessionController extends Controller { @@ -23,7 +27,7 @@ class TwoFactorAuthenticatedSessionController extends Controller /** * Create a new controller instance. * - * @param \Illuminate\Contracts\Auth\StatefulGuard $guard + * @param \Illuminate\Contracts\Auth\StatefulGuard $guard * @return void */ public function __construct(StatefulGuard $guard) @@ -34,12 +38,12 @@ public function __construct(StatefulGuard $guard) /** * Show the two factor authentication challenge view. * - * @param \Laravel\Fortify\Http\Requests\TwoFactorLoginRequest $request + * @param \Laravel\Fortify\Http\Requests\TwoFactorLoginRequest $request * @return \Laravel\Fortify\Contracts\TwoFactorChallengeViewResponse */ public function create(TwoFactorLoginRequest $request): TwoFactorChallengeViewResponse { - if (! $request->hasChallengedUser()) { + if (!$request->hasChallengedUser()) { throw new HttpResponseException(redirect()->route('admin.login')); } @@ -49,25 +53,32 @@ public function create(TwoFactorLoginRequest $request): TwoFactorChallengeViewRe /** * Attempt to authenticate a new session using the two factor authentication code. * - * @param \Laravel\Fortify\Http\Requests\TwoFactorLoginRequest $request + * @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()) { + } elseif (!$request->hasValidCode()) { return app(FailedTwoFactorLoginResponse::class)->toResponse($request); } - $this->guard->login($user, $request->remember()); $request->session()->regenerate(); - return app(TwoFactorLoginResponse::class); } + + public function adminTwoFactorLogout(Request $request) + { + Auth::guard('admin')->logout(); + + $request->session()->invalidate(); + + $request->session()->regenerateToken(); + return (Session::get('active_two_factor') ? redirect()->route('admin.login') : redirect()->route('login')); + } } diff --git a/app/Actions/Fortify/PrepareAuthenticatedSession.php b/app/Actions/Fortify/PrepareAuthenticatedSession.php new file mode 100644 index 0000000..c3c7f32 --- /dev/null +++ b/app/Actions/Fortify/PrepareAuthenticatedSession.php @@ -0,0 +1,54 @@ +guard = $guard; + $this->limiter = $limiter; + } + + /** + * Handle the incoming request. + * + * @param \Illuminate\Http\Request $request + * @param callable $next + * @return mixed + */ + public function handle($request, $next) + { + $request->session()->regenerate(); + + $this->limiter->clear($request); + + + return $next($request); + } +} diff --git a/app/Actions/Fortify/RedirectIfTwoFactorAuthenticatable.php b/app/Actions/Fortify/RedirectIfTwoFactorAuthenticatable.php index c29b6c0..c64ae10 100644 --- a/app/Actions/Fortify/RedirectIfTwoFactorAuthenticatable.php +++ b/app/Actions/Fortify/RedirectIfTwoFactorAuthenticatable.php @@ -5,6 +5,7 @@ use Illuminate\Auth\Events\Failed; use Illuminate\Contracts\Auth\StatefulGuard; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Session; use Illuminate\Validation\ValidationException; use Laravel\Fortify\Events\TwoFactorAuthenticationChallenged; use Laravel\Fortify\Fortify; @@ -39,6 +40,8 @@ public function __construct(StatefulGuard $guard, LoginRateLimiter $limiter) $this->guard = $guard; $this->limiter = $limiter; + $model = $this->guard->getProvider()->getModel(); + } /** @@ -50,8 +53,9 @@ public function __construct(StatefulGuard $guard, LoginRateLimiter $limiter) */ public function handle($request, $next) { - $user = $this->validateCredentials($request); + $user = $this->validateCredentials($request); + Session::put('active_two_factor','admin'); if (Fortify::confirmsTwoFactorAuthentication()) { if (optional($user)->two_factor_secret && !is_null(optional($user)->two_factor_confirmed_at) && @@ -146,6 +150,8 @@ protected function twoFactorChallengeResponse($request, $user) ]); TwoFactorAuthenticationChallenged::dispatch($user); + $table = $user->getTable(); + Session::put('table', $table); if ($user->getTable() == 'admins') { @@ -153,6 +159,7 @@ protected function twoFactorChallengeResponse($request, $user) ? 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/Fortify/Requests/TwoFactorLoginRequest.php b/app/Actions/Fortify/Requests/TwoFactorLoginRequest.php new file mode 100644 index 0000000..56ad073 --- /dev/null +++ b/app/Actions/Fortify/Requests/TwoFactorLoginRequest.php @@ -0,0 +1,141 @@ + 'nullable|string', + 'recovery_code' => 'nullable|string', + ]; + } + + /** + * Determine if the request has a valid two factor code. + * + * @return bool + */ + public function hasValidCode() + { + return $this->code && tap(app(TwoFactorAuthenticationProvider::class)->verify( + decrypt($this->challengedUser()->two_factor_secret), $this->code + ), function ($result) { + if ($result) { + $this->session()->forget('login.id'); + } + }); + } + + /** + * Get the valid recovery code if one exists on the request. + * + * @return string|null + */ + public function validRecoveryCode() + { + if (! $this->recovery_code) { + return; + } + + return tap(collect($this->challengedUser()->recoveryCodes())->first(function ($code) { + return hash_equals($this->recovery_code, $code) ? $code : null; + }), function ($code) { + if ($code) { + $this->session()->forget('login.id'); + } + }); + } + + /** + * Determine if there is a challenged user in the current session. + * + * @return bool + */ + public function hasChallengedUser() + { + if ($this->challengedUser) { + return true; + } + +// $model = app(StatefulGuard::class)->getProvider()->getModel(); + $model = 'App\Models\Admin'; + + return $this->session()->has('login.id') && + $model::find($this->session()->get('login.id')); + } + + /** + * Get the user that is attempting the two factor challenge. + * + * @return mixed + */ + public function challengedUser() + { + if ($this->challengedUser) { + return $this->challengedUser; + } + +// $model = app(StatefulGuard::class)->getProvider()->getModel(); + $model = 'App\Models\Admin'; + if (! $this->session()->has('login.id') || + ! $user = $model::find($this->session()->get('login.id'))) { + throw new HttpResponseException( + app(FailedTwoFactorLoginResponse::class)->toResponse($this) + ); + } + + return $this->challengedUser = $user; + } + + /** + * Determine if the user wanted to be remembered after login. + * + * @return bool + */ + public function remember() + { + if (! $this->remember) { + $this->remember = $this->session()->pull('login.remember', false); + } + + return $this->remember; + } +} diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index c122543..df7e64e 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -8,8 +8,9 @@ use Illuminate\Routing\Pipeline; use App\Actions\Fortify\AttemptToAuthenticate; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Session; use Laravel\Fortify\Actions\EnsureLoginIsNotThrottled; -use Laravel\Fortify\Actions\PrepareAuthenticatedSession; +use App\Actions\Fortify\PrepareAuthenticatedSession; use App\Actions\Fortify\RedirectIfTwoFactorAuthenticatable; use App\Http\Responses\AdminLoginResponse; use Laravel\Fortify\Contracts\LoginViewResponse; @@ -37,6 +38,7 @@ public function __construct(StatefulGuard $guard) { $this->guard = $guard; + } public function loginForm(){ @@ -76,6 +78,7 @@ public function store(LoginRequest $request) */ protected function loginPipeline(LoginRequest $request) { + if (Fortify::$authenticateThroughCallback) { return (new \Illuminate\Pipeline\Pipeline(app()))->send($request)->through(array_filter( call_user_func(Fortify::$authenticateThroughCallback, $request) @@ -90,9 +93,11 @@ protected function loginPipeline(LoginRequest $request) return (new Pipeline(app()))->send($request)->through(array_filter([ config('fortify.limiters.login') ? null : EnsureLoginIsNotThrottled::class, + Features::enabled(Features::twoFactorAuthentication()) ? RedirectIfTwoFactorAuthenticatable::class : null, AttemptToAuthenticate::class, PrepareAuthenticatedSession::class, + ])); } @@ -104,12 +109,16 @@ protected function loginPipeline(LoginRequest $request) */ public function destroy(Request $request): AdminLogoutResponse { + Auth::guard('admin')->logout(); $request->session()->invalidate(); $request->session()->regenerateToken(); + return app(AdminLogoutResponse::class); } + + } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 90f75cc..4d83a28 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -8,6 +8,7 @@ use Illuminate\Routing\Pipeline; use App\Actions\Fortify\AttemptToAuthenticate; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Session; use Laravel\Fortify\Actions\EnsureLoginIsNotThrottled; use Laravel\Fortify\Actions\PrepareAuthenticatedSession; use App\Actions\Fortify\RedirectIfTwoFactorAuthenticatable; @@ -36,6 +37,8 @@ class UserController extends Controller public function __construct(StatefulGuard $guard) { $this->guard = $guard; + $model = ($guard->getProvider()->getModel()); + if ($model === 'App\Models\User') Session::put('guard', 'web'); } public function loginForm(){ diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 1ca3258..7cc7a77 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -69,6 +69,7 @@ class Kernel extends HttpKernel 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'admin' => \App\Http\Middleware\AdminRedirectIfAuthenticated::class, + 'guard-check'=> \App\Http\Middleware\CheckGuard::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'signed' => \App\Http\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, diff --git a/app/Http/Middleware/AdminRedirectIfAuthenticated.php b/app/Http/Middleware/AdminRedirectIfAuthenticated.php index 6b53afb..2e88eb0 100644 --- a/app/Http/Middleware/AdminRedirectIfAuthenticated.php +++ b/app/Http/Middleware/AdminRedirectIfAuthenticated.php @@ -6,6 +6,7 @@ use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Session; class AdminRedirectIfAuthenticated { @@ -22,7 +23,7 @@ public function handle(Request $request, Closure $next, ...$guards) $guards = empty($guards) ? [null] : $guards; foreach ($guards as $guard) { - if (Auth::guard($guard)->check()) { + if (Auth::guard('admin')->check()) { return redirect($guard.'/dashboard'); } } diff --git a/app/Http/Middleware/CheckGuard.php b/app/Http/Middleware/CheckGuard.php new file mode 100644 index 0000000..d149737 --- /dev/null +++ b/app/Http/Middleware/CheckGuard.php @@ -0,0 +1,26 @@ +route('admin.two-factor.login'); + } + return $next($request); + } +} diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index a2813a0..0b00e1d 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -22,7 +22,7 @@ public function handle(Request $request, Closure $next, ...$guards) $guards = empty($guards) ? [null] : $guards; foreach ($guards as $guard) { - if (Auth::guard($guard)->check()) { + if (Auth::guard('web')->check()) { return redirect(RouteServiceProvider::HOME); } } diff --git a/app/Models/Admin.php b/app/Models/Admin.php index 9bce814..5756fea 100644 --- a/app/Models/Admin.php +++ b/app/Models/Admin.php @@ -63,6 +63,10 @@ class Admin extends Authenticatable implements MustVerifyEmail 'profile_photo_url', ]; + public function isAdmin(): bool{ + return true; + } + public function sendPasswordResetNotification($token) { $this->notify(new ResetPassword($token)); diff --git a/app/Models/User.php b/app/Models/User.php index 5ccb65b..a5d9d62 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -58,4 +58,9 @@ class User extends Authenticatable implements MustVerifyEmail protected $appends = [ 'profile_photo_url', ]; + + public function isAdmin(): bool + { + return false; + } } diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index 1d1bf36..f9fe264 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -4,6 +4,11 @@ use App\Actions\Fortify\ConfirmPassword; use App\Actions\Fortify\Controllers\ProfileInformationController; use App\Actions\Fortify\Controllers\TwoFactorAuthenticatedSessionController; +use App\Actions\Fortify\Requests\TwoFactorLoginRequest; +use Illuminate\Support\Facades\Session; +use App\Actions\Fortify\PrepareAuthenticatedSession; +use Laravel\Fortify\Contracts\FailedTwoFactorLoginResponse; +use Laravel\Fortify\Contracts\TwoFactorLoginResponse; use Laravel\Fortify\Fortify; use App\Actions\Fortify\CreateNewUser; @@ -32,12 +37,44 @@ class FortifyServiceProvider extends ServiceProvider public function register() { $this->app - ->when([AdminController::class, AttemptToAuthenticate::class, RedirectIfTwoFactorAuthenticatable::class,ConfirmPassword::class, - PasswordResetLinkController::class, NewPasswordController::class,ProfileInformationController::class, UpdateUserProfileInformation::class,TwoFactorAuthenticatedSessionController::class]) + ->when([AdminController::class, AttemptToAuthenticate::class, RedirectIfTwoFactorAuthenticatable::class,ConfirmPassword::class,PrepareAuthenticatedSession::class, + PasswordResetLinkController::class, NewPasswordController::class,ProfileInformationController::class, UpdateUserProfileInformation::class,StatefulGuard::class, + TwoFactorAuthenticatedSessionController::class,TwoFactorLoginRequest::class]) ->needs(StatefulGuard::class) ->give(function () { return Auth::guard('admin'); }); + + $this->app->instance(FailedTwoFactorLoginResponse::class, new class implements FailedTwoFactorLoginResponse { + public function toResponse($request) + { + if(Session::get('active_two_factor') === 'admin'){ + return redirect('admin/two-factor-challenge'); + }else{ + return redirect('/two-factor-challenge'); + } + + + } + }); + + + + $this->app->instance(TwoFactorLoginResponse::class, new class implements TwoFactorLoginResponse { + public function toResponse($request) + { + if(Session::get('active_two_factor') === 'admin'){ + Session::forget('active_two_factor'); + return redirect('admin/dashboard'); + }else{ + Session::forget('active_two_factor'); + return redirect('/dashboard'); + } + + } + }); + + } /** diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 7b415d1..a83a4f6 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -18,6 +18,7 @@ class RouteServiceProvider extends ServiceProvider * @var string */ public const HOME = '/dashboard'; + public const ADMIN_HOME = '/admin/dashboard'; public static function redirectTo($guard): string { diff --git a/resources/views/auth/two-factor-challenge.blade.php b/resources/views/auth/two-factor-challenge.blade.php index 6d54913..f243d9a 100644 --- a/resources/views/auth/two-factor-challenge.blade.php +++ b/resources/views/auth/two-factor-challenge.blade.php @@ -1,10 +1,27 @@ - +
+
+ +
+ @csrf + +
+ +
+ + +
{{ __('Please confirm access to your account by entering the authentication code provided by your authenticator application.') }}
@@ -13,25 +30,28 @@ {{ __('Please confirm access to your account by entering one of your emergency recovery codes.') }}
- + -
+ @csrf
- - + +
- - + +
+
diff --git a/routes/admin.php b/routes/admin.php index 6d029de..0545225 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -24,8 +24,9 @@ 'prefix' => 'admin', 'as' => 'admin.' ], function () { - Route::get('login', [\App\Http\Controllers\AdminController::class, 'loginForm'])->name('login'); + Route::middleware('guard-check')->get('login', [\App\Http\Controllers\AdminController::class, 'loginForm'])->name('login'); Route::post('login', [\App\Http\Controllers\AdminController::class, 'store']); + Route::post('two-factor-logout', [TwoFactorAuthenticatedSessionController::class, 'adminTwoFactorLogout'])->name('twoFactorLogout'); // Route::get('forgot-password', [\App\Actions\Fortify\Controllers\PasswordResetLinkController::class, 'create'])->name('password.request'); // Route::post('forgot-password', [\App\Actions\Fortify\Controllers\PasswordResetLinkController::class, 'store'])->name('password.email'); // Route::get('reset-password/{token}', [\App\Actions\Fortify\Controllers\NewPasswordController::class, 'create'])->name('password.reset'); @@ -34,7 +35,8 @@ Route::get('/reset-password/{token}', [\App\Actions\Fortify\Controllers\NewPasswordController::class, 'create'])->name('password.reset'); Route::post('/forgot-password', [\App\Actions\Fortify\Controllers\PasswordResetLinkController::class, 'store'])->name('password.email'); Route::post('/reset-password', [\App\Actions\Fortify\Controllers\NewPasswordController::class, 'store'])->name('password.update'); - + Route::get('/two-factor-challenge', [TwoFactorAuthenticatedSessionController::class, 'create'])->name('two-factor.login'); + Route::post('/two-factor-challenge', [TwoFactorAuthenticatedSessionController::class, 'store'])->name('two-factor.store');; // Route::put('/user/profile-information', [ProfileInformationController::class, 'update']) // ->middleware([config('fortify.auth_middleware', 'auth').':'.config('fortify.guard')]) @@ -58,7 +60,7 @@ Route::get('/profile', [\App\Http\Controllers\Livewire\AdminProfileController::class, 'show'])->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'); diff --git a/routes/web.php b/routes/web.php index ba81804..fa8a615 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,7 @@ 'guest:web', +], function () { + Route::middleware('guard-check')->get('/login', [\Laravel\Fortify\Http\Controllers\AuthenticatedSessionController::class, 'create'])->name('login'); + Route::middleware('guard-check')->get('/two-factor-challenge', [TwoFactorAuthenticatedSessionController::class, 'create'])->name('two-factor.login'); +}); + + Route::middleware([ 'auth:sanctum,web', config('jetstream.auth_session'),