BookStack/app/Http/Controllers/Auth/MfaBackupCodesController.php
Dan Brown 9b271e559f
Worked on MFA setup required flow
- Restructured some of the route naming to be a little more consistent.
- Moved the routes about to be more logically in one place.
- Created a new middleware to handle the auth of people that should be
  allowed access to mfa setup routes, since these could be used by
  existing logged in users or by people needing to setup MFA on access.
- Added testing to cover MFA setup required flow.
- Added TTL and method tracking to session last-login tracking system.
2021-08-02 22:02:25 +01:00

90 lines
3.1 KiB
PHP

<?php
namespace BookStack\Http\Controllers\Auth;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Access\LoginService;
use BookStack\Auth\Access\Mfa\BackupCodeService;
use BookStack\Auth\Access\Mfa\MfaSession;
use BookStack\Auth\Access\Mfa\MfaValue;
use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controllers\Controller;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class MfaBackupCodesController extends Controller
{
use HandlesPartialLogins;
protected const SETUP_SECRET_SESSION_KEY = 'mfa-setup-backup-codes';
/**
* Show a view that generates and displays backup codes
*/
public function generate(BackupCodeService $codeService)
{
$codes = $codeService->generateNewSet();
session()->put(self::SETUP_SECRET_SESSION_KEY, encrypt($codes));
$downloadUrl = 'data:application/octet-stream;base64,' . base64_encode(implode("\n\n", $codes));
return view('mfa.backup-codes-generate', [
'codes' => $codes,
'downloadUrl' => $downloadUrl,
]);
}
/**
* Confirm the setup of backup codes, storing them against the user.
* @throws Exception
*/
public function confirm()
{
if (!session()->has(self::SETUP_SECRET_SESSION_KEY)) {
return response('No generated codes found in the session', 500);
}
$codes = decrypt(session()->pull(self::SETUP_SECRET_SESSION_KEY));
MfaValue::upsertWithValue($this->currentOrLastAttemptedUser(), MfaValue::METHOD_BACKUP_CODES, json_encode($codes));
$this->logActivity(ActivityType::MFA_SETUP_METHOD, 'backup-codes');
return redirect('/mfa/setup');
}
/**
* Verify the MFA method submission on check.
* @throws NotFoundException
* @throws ValidationException
*/
public function verify(Request $request, BackupCodeService $codeService, MfaSession $mfaSession, LoginService $loginService)
{
$user = $this->currentOrLastAttemptedUser();
$codes = MfaValue::getValueForUser($user, MfaValue::METHOD_BACKUP_CODES) ?? '[]';
$this->validate($request, [
'code' => [
'required',
'max:12', 'min:8',
function ($attribute, $value, $fail) use ($codeService, $codes) {
if (!$codeService->inputCodeExistsInSet($value, $codes)) {
$fail(trans('validation.backup_codes'));
}
}
]
]);
$updatedCodes = $codeService->removeInputCodeFromSet($request->get('code'), $codes);
MfaValue::upsertWithValue($user, MfaValue::METHOD_BACKUP_CODES, $updatedCodes);
$mfaSession->markVerifiedForUser($user);
$loginService->reattemptLoginFor($user);
if ($codeService->countCodesInSet($updatedCodes) < 5) {
$this->showWarningNotification('You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.');
}
return redirect()->intended();
}
}