diff --git a/app/Auth/Access/Mfa/MfaSession.php b/app/Auth/Access/Mfa/MfaSession.php new file mode 100644 index 000000000..67574cbaf --- /dev/null +++ b/app/Auth/Access/Mfa/MfaSession.php @@ -0,0 +1,44 @@ +mfaValues()->exists() || $this->currentUserRoleEnforcesMfa(); + } + + /** + * Check if a role of the current user enforces MFA. + */ + protected function currentUserRoleEnforcesMfa(): bool + { + return user()->roles() + ->where('mfa_enforced', '=', true) + ->exists(); + } + + /** + * Check if the current MFA session has already been verified. + */ + public function isVerified(): bool + { + return session()->get(self::MFA_VERIFIED_SESSION_KEY) === 'true'; + } + + /** + * Mark the current session as MFA-verified. + */ + public function markVerified(): void + { + session()->put(self::MFA_VERIFIED_SESSION_KEY, 'true'); + } + +} \ No newline at end of file diff --git a/app/Http/Controllers/Auth/MfaController.php b/app/Http/Controllers/Auth/MfaController.php index 9feda9433..39a4e852f 100644 --- a/app/Http/Controllers/Auth/MfaController.php +++ b/app/Http/Controllers/Auth/MfaController.php @@ -37,4 +37,18 @@ class MfaController extends Controller return redirect('/mfa/setup'); } + + /** + * Show the page to start an MFA verification. + */ + public function verify() + { + $userMethods = user()->mfaValues() + ->get(['id', 'method']) + ->groupBy('method'); + + return view('mfa.verify', [ + 'userMethods' => $userMethods, + ]); + } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 4f9bfc1e6..c9e59ed3e 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -48,6 +48,7 @@ class Kernel extends HttpKernel */ protected $routeMiddleware = [ 'auth' => \BookStack\Http\Middleware\Authenticate::class, + 'mfa' => \BookStack\Http\Middleware\EnforceMfaRequirements::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, diff --git a/app/Http/Middleware/EnforceMfaRequirements.php b/app/Http/Middleware/EnforceMfaRequirements.php index 957b42ae1..ac3c9609b 100644 --- a/app/Http/Middleware/EnforceMfaRequirements.php +++ b/app/Http/Middleware/EnforceMfaRequirements.php @@ -2,10 +2,21 @@ namespace BookStack\Http\Middleware; +use BookStack\Auth\Access\Mfa\MfaSession; use Closure; class EnforceMfaRequirements { + protected $mfaSession; + + /** + * EnforceMfaRequirements constructor. + */ + public function __construct(MfaSession $mfaSession) + { + $this->mfaSession = $mfaSession; + } + /** * Handle an incoming request. * @@ -15,10 +26,23 @@ class EnforceMfaRequirements */ public function handle($request, Closure $next) { - $mfaRequired = user()->roles()->where('mfa_enforced', '=', true)->exists(); - // TODO - Run this after auth (If authenticated) - // TODO - Redirect user to setup MFA or verify via MFA. + if ( + !$this->mfaSession->isVerified() + && !$request->is('mfa/verify*', 'uploads/images/user/*') + && $this->mfaSession->requiredForCurrentUser() + ) { + return redirect('/mfa/verify'); + } + + // TODO - URI wildcard exceptions above allow access to the 404 page of this user + // which could then expose content. Either need to lock that down (Tricky to do image thing) + // or prevent any level of auth until verified. + + // TODO - Need to redirect to setup if not configured AND ONLY IF NO OPTIONS CONFIGURED + // Might need to change up such routes to start with /configure/ for such identification. + // (Can't allow access to those if already configured) // TODO - Store mfa_pass into session for future requests? + return $next($request); } } diff --git a/resources/views/mfa/verify.blade.php b/resources/views/mfa/verify.blade.php new file mode 100644 index 000000000..4ff0e6c49 --- /dev/null +++ b/resources/views/mfa/verify.blade.php @@ -0,0 +1,31 @@ +@extends('simple-layout') + +@section('body') +
+ +
+

Verify Access

+

+ Your user account requires you to confirm your identity via an additional level + of verification before you're granted access. + Verify using one of your configure methods to continue. +

+ +
+
+
+
METHOD A
+

+ ... +

+
+
+ BUTTON +
+
+ +
+ +
+
+@stop diff --git a/routes/web.php b/routes/web.php index e3329edc4..406cfd767 100644 --- a/routes/web.php +++ b/routes/web.php @@ -4,7 +4,7 @@ Route::get('/status', 'StatusController@show'); Route::get('/robots.txt', 'HomeController@getRobots'); // Authenticated routes... -Route::group(['middleware' => 'auth'], function () { +Route::group(['middleware' => ['auth', 'mfa']], function () { // Secure images routing Route::get('/uploads/images/{path}', 'Images\ImageController@showImage') @@ -224,13 +224,14 @@ Route::group(['middleware' => 'auth'], function () { Route::put('/roles/{id}', 'RoleController@update'); }); - // MFA Setup Routes + // MFA Routes Route::get('/mfa/setup', 'Auth\MfaController@setup'); Route::get('/mfa/totp-generate', 'Auth\MfaTotpController@generate'); Route::post('/mfa/totp-confirm', 'Auth\MfaTotpController@confirm'); Route::get('/mfa/backup-codes-generate', 'Auth\MfaBackupCodesController@generate'); Route::post('/mfa/backup-codes-confirm', 'Auth\MfaBackupCodesController@confirm'); Route::delete('/mfa/remove/{method}', 'Auth\MfaController@remove'); + Route::get('/mfa/verify', 'Auth\MfaController@verify'); }); // Social auth routes