diff --git a/app/Http/Controllers/Auth/MfaController.php b/app/Http/Controllers/Auth/MfaController.php index 1d0dbd1a4..be381e3b0 100644 --- a/app/Http/Controllers/Auth/MfaController.php +++ b/app/Http/Controllers/Auth/MfaController.php @@ -2,11 +2,24 @@ namespace BookStack\Http\Controllers\Auth; +use BaconQrCode\Renderer\Color\Rgb; +use BaconQrCode\Renderer\Image\SvgImageBackEnd; +use BaconQrCode\Renderer\ImageRenderer; +use BaconQrCode\Renderer\RendererStyle\Fill; +use BaconQrCode\Renderer\RendererStyle\RendererStyle; +use BaconQrCode\Writer; use BookStack\Http\Controllers\Controller; use Illuminate\Http\Request; +use Illuminate\Validation\ValidationException; +use PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException; +use PragmaRX\Google2FA\Exceptions\InvalidCharactersException; +use PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException; +use PragmaRX\Google2FA\Google2FA; class MfaController extends Controller { + protected const TOTP_SETUP_SECRET_SESSION_KEY = 'mfa-setup-totp-secret'; + /** * Show the view to setup MFA for the current user. */ @@ -17,13 +30,57 @@ class MfaController extends Controller return view('mfa.setup'); } - public function generateQr() + /** + * Show a view that generates and displays a TOTP QR code. + * @throws IncompatibleWithGoogleAuthenticatorException + * @throws InvalidCharactersException + * @throws SecretKeyTooShortException + */ + public function totpGenerate() { - // https://github.com/antonioribeiro/google2fa#how-to-generate-and-use-two-factor-authentication + // TODO - Ensure a QR code doesn't already exist? Or overwrite? + $google2fa = new Google2FA(); + if (session()->has(static::TOTP_SETUP_SECRET_SESSION_KEY)) { + $totpSecret = decrypt(session()->get(static::TOTP_SETUP_SECRET_SESSION_KEY)); + } else { + $totpSecret = $google2fa->generateSecretKey(); + session()->put(static::TOTP_SETUP_SECRET_SESSION_KEY, encrypt($totpSecret)); + } + + $qrCodeUrl = $google2fa->getQRCodeUrl( + setting('app-name'), + user()->email, + $totpSecret + ); + + $color = Fill::uniformColor(new Rgb(255, 255, 255), new Rgb(32, 110, 167)); + $svg = (new Writer( + new ImageRenderer( + new RendererStyle(192, 0, null, null, $color), + new SvgImageBackEnd + ) + ))->writeString($qrCodeUrl); - // Generate secret key - // Store key in session? // Get user to verify setup via responding once. // If correct response, Save key against user + return view('mfa.totp-generate', [ + 'secret' => $totpSecret, + 'svg' => $svg, + ]); + } + + /** + * Confirm the setup of TOTP and save the auth method secret + * against the current user. + * @throws ValidationException + */ + public function totpConfirm(Request $request) + { + $this->validate($request, [ + 'code' => 'required|max:12|min:4' + ]); + + // TODO - Confirm code + dd($request->input('code')); } } diff --git a/resources/sass/_forms.scss b/resources/sass/_forms.scss index 953d1d060..bb6d17f82 100644 --- a/resources/sass/_forms.scss +++ b/resources/sass/_forms.scss @@ -29,6 +29,10 @@ } } +.input-fill-width { + width: 100% !important; +} + .fake-input { @extend .input-base; overflow: auto; diff --git a/resources/views/mfa/setup.blade.php b/resources/views/mfa/setup.blade.php index 25eb5d925..577841af5 100644 --- a/resources/views/mfa/setup.blade.php +++ b/resources/views/mfa/setup.blade.php @@ -5,12 +5,39 @@ <div class="card content-wrap auto-height"> <h1 class="list-heading">Setup Multi-Factor Authentication</h1> - <p> + <p class="mb-none"> Setup multi-factor authentication as an extra layer of security for your user account. - To use multi-factor authentication you'll need a mobile application - that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator. </p> + + <div class="setting-list"> + <div class="grid half gap-xl"> + <div> + <div class="setting-list-label">Mobile App</div> + <p class="small"> + To use multi-factor authentication you'll need a mobile application + that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator. + </p> + </div> + <div class="pt-m"> + <a href="{{ url('/mfa/totp-generate') }}" class="button outline">Setup</a> + </div> + </div> + + <div class="grid half gap-xl"> + <div> + <div class="setting-list-label">Backup Codes</div> + <p class="small"> + Print out or securely store a set of one-time backup codes + which you can enter to verify your identity. + </p> + </div> + <div class="pt-m"> + <a href="{{ url('/mfa/codes/generate') }}" class="button outline">Setup</a> + </div> + </div> + </div> + </div> </div> @stop diff --git a/resources/views/mfa/totp-generate.blade.php b/resources/views/mfa/totp-generate.blade.php new file mode 100644 index 000000000..17d38adaa --- /dev/null +++ b/resources/views/mfa/totp-generate.blade.php @@ -0,0 +1,44 @@ +@extends('simple-layout') + +@section('body') + + <div class="container very-small py-xl"> + <div class="card content-wrap auto-height"> + <h1 class="list-heading">Mobile App Setup</h1> + <p> + To use multi-factor authentication you'll need a mobile application + that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator. + </p> + <p> + Scan the QR code below using your preferred authentication app to get started. + </p> + + <div class="text-center"> + <div class="block inline"> + {!! $svg !!} + </div> + </div> + + <h2 class="list-heading">Verify Setup</h2> + <p id="totp-verify-input-details" class="mb-s"> + Verify that all is working by entering a code, generated within your + authentication app, in the input box below: + </p> + <form action="{{ url('/mfa/totp-confirm') }}" method="POST"> + {{ csrf_field() }} + <input type="text" + name="code" + aria-labelledby="totp-verify-input-details" + placeholder="Provide your app generated code here" + class="input-fill-width {{ $errors->has('code') ? 'neg' : '' }}"> + @if($errors->has('code')) + <div class="text-neg text-small px-xs">{{ $errors->first('code') }}</div> + @endif + <div class="mt-s text-right"> + <button class="button">Confirm and Enable</button> + </div> + </form> + </div> + </div> + +@stop diff --git a/routes/web.php b/routes/web.php index 7807d5477..f9967465b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -225,6 +225,8 @@ Route::group(['middleware' => 'auth'], function () { }); Route::get('/mfa/setup', 'Auth\MfaController@setup'); + Route::get('/mfa/totp-generate', 'Auth\MfaController@totpGenerate'); + Route::post('/mfa/totp-confirm', 'Auth\MfaController@totpConfirm'); }); // Social auth routes