Merge pull request #204 from zjean/2fa

Validate the 2fa secret
This commit is contained in:
James Cole 2016-03-07 20:01:19 +01:00
commit 8828aa0621
13 changed files with 217 additions and 13 deletions

View File

@ -1,11 +1,15 @@
<?php namespace FireflyIII\Http\Controllers; <?php namespace FireflyIII\Http\Controllers;
use Auth;
use Config; use Config;
use FireflyIII\Repositories\Account\AccountRepositoryInterface as ARI; use FireflyIII\Repositories\Account\AccountRepositoryInterface as ARI;
use FireflyIII\Http\Requests\TokenFormRequest;
use Input; use Input;
use Preferences; use Preferences;
use Response;
use Session; use Session;
use View; use View;
use PragmaRX\Google2FA\Contracts\Google2FA;
/** /**
* Class PreferencesController * Class PreferencesController
@ -43,12 +47,15 @@ class PreferencesController extends Controller
$fiscalYearStartStr = Preferences::get('fiscalYearStart', '01-01')->data; $fiscalYearStartStr = Preferences::get('fiscalYearStart', '01-01')->data;
$fiscalYearStart = date('Y') . '-' . $fiscalYearStartStr; $fiscalYearStart = date('Y') . '-' . $fiscalYearStartStr;
$twoFactorAuthEnabled = Preferences::get('twoFactorAuthEnabled', 0)->data; $twoFactorAuthEnabled = Preferences::get('twoFactorAuthEnabled', 0)->data;
$hasTwoFactorAuthSecret = Preferences::get('twoFactorAuthSecret') != null && !empty(Preferences::get('twoFactorAuthSecret')->data);
$showIncomplete = env('SHOW_INCOMPLETE_TRANSLATIONS', false) === true; $showIncomplete = env('SHOW_INCOMPLETE_TRANSLATIONS', false) === true;
return view( return view(
'preferences.index', 'preferences.index',
compact( compact(
'budgetMaximum', 'language', 'accounts', 'frontPageAccounts', 'viewRange', 'customFiscalYear', 'fiscalYearStart', 'twoFactorAuthEnabled', 'budgetMaximum', 'language', 'accounts', 'frontPageAccounts', 'viewRange', 'customFiscalYear', 'fiscalYearStart', 'twoFactorAuthEnabled', 'hasTwoFactorAuthSecret',
'showIncomplete' 'showIncomplete'
) )
); );
@ -87,7 +94,14 @@ class PreferencesController extends Controller
// two factor auth // two factor auth
$twoFactorAuthEnabled = (int)Input::get('twoFactorAuthEnabled'); $twoFactorAuthEnabled = (int)Input::get('twoFactorAuthEnabled');
Preferences::set('twoFactorAuthEnabled', $twoFactorAuthEnabled);
$hasTwoFactorAuthSecret = Preferences::get('twoFactorAuthSecret') != null && !empty(Preferences::get('twoFactorAuthSecret')->data);
// If we already have a secret, just set the two factor auth enabled to 1, and let the user continue with the existing secret.
if($hasTwoFactorAuthSecret)
{
Preferences::set('twoFactorAuthEnabled', $twoFactorAuthEnabled);
}
// language: // language:
$lang = Input::get('language'); $lang = Input::get('language');
@ -99,7 +113,44 @@ class PreferencesController extends Controller
Session::flash('success', 'Preferences saved!'); Session::flash('success', 'Preferences saved!');
Preferences::mark(); Preferences::mark();
// if we don't have a valid secret yet, redirect to the code page.
if(!$hasTwoFactorAuthSecret)
{
return redirect(route('preferences.code'));
}
return redirect(route('preferences')); return redirect(route('preferences'));
} }
/*
* @param TokenFormRequest $request
*
* @return $this|\Illuminate\View\View
*/
public function postCode(TokenFormRequest $request)
{
Preferences::set('twoFactorAuthEnabled', 1);
Preferences::set('twoFactorAuthSecret', $request->input('secret'));
Session::flash('success', 'Preferences saved!');
Preferences::mark();
return redirect(route('preferences'));
}
/*
* @param Google2FA $google2fa
*
* @return $this|\Illuminate\View\View
*/
public function code(Google2FA $google2fa)
{
$secret = $google2fa->generateSecretKey(16, Auth::user()->id);
$image = $google2fa->getQRCodeInline("FireflyIII", null, $secret, 150);
return view('preferences.code', compact('secret', 'image'));
}
} }

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types = 1);
namespace FireflyIII\Http\Requests;
use Auth;
use Input;
/**
* Class TokenFormRequest
*
*
* @package FireflyIII\Http\Requests
*/
class TokenFormRequest extends Request
{
/**
* @return bool
*/
public function authorize()
{
// Only allow logged in users
return Auth::check();
}
/**
* @return array
*/
public function rules()
{
$rules = [
'secret' => 'required',
'code' => 'required|2faCode:secret',
];
return $rules;
}
}

View File

@ -378,6 +378,14 @@ Breadcrumbs::register(
} }
); );
Breadcrumbs::register(
'preferences.code', function (BreadCrumbGenerator $breadcrumbs) {
$breadcrumbs->parent('home');
$breadcrumbs->push(trans('breadcrumbs.preferences'), route('preferences'));
}
);
/** /**
* PROFILE * PROFILE
*/ */

View File

@ -246,6 +246,8 @@ Route::group(
*/ */
Route::get('/preferences', ['uses' => 'PreferencesController@index', 'as' => 'preferences']); Route::get('/preferences', ['uses' => 'PreferencesController@index', 'as' => 'preferences']);
Route::post('/preferences', ['uses' => 'PreferencesController@postIndex']); Route::post('/preferences', ['uses' => 'PreferencesController@postIndex']);
Route::get('/preferences/code', ['uses' => 'PreferencesController@code', 'as' => 'preferences.code']);
Route::post('/preferences/code', ['uses' => 'PreferencesController@postCode']);
/** /**
* Profile Controller * Profile Controller

View File

@ -16,10 +16,13 @@ use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Rules\Triggers\TriggerInterface; use FireflyIII\Rules\Triggers\TriggerInterface;
use FireflyIII\User; use FireflyIII\User;
use Input;
use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Validation\Validator; use Illuminate\Validation\Validator;
use Log; use Log;
use PragmaRX\Google2FA\Contracts\Google2FA;
use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Translation\TranslatorInterface;
use Session;
/** /**
* Class FireflyValidator * Class FireflyValidator
@ -43,6 +46,29 @@ class FireflyValidator extends Validator
parent::__construct($translator, $data, $rules, $messages, $customAttributes); parent::__construct($translator, $data, $rules, $messages, $customAttributes);
} }
/**
* @param $attribute
* @param $value
* @param $parameters
*
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*
* @return bool
*/
public function validate2faCode($attribute, $value, $parameters): bool
{
if (!is_string($value) || is_null($value) || strlen($value) <> 6) {
return false;
}
// Retrieve the secret from our hidden form field.
$secret = Input::get($parameters[0]);
$google2fa = app('PragmaRX\Google2FA\Google2FA');
return $google2fa->verifyKey($secret, $value);
}
/** /**
* @param $attribute * @param $attribute
* @param $value * @param $value

View File

@ -22,7 +22,8 @@
"rcrowe/twigbridge": "~0.9", "rcrowe/twigbridge": "~0.9",
"league/csv": "^7.1", "league/csv": "^7.1",
"laravelcollective/html": "^5.2", "laravelcollective/html": "^5.2",
"rmccue/requests": "^1.6" "rmccue/requests": "^1.6",
"pragmarx/google2fa": "^0.7.1"
}, },
"require-dev": { "require-dev": {
"fzaninotto/faker": "~1.4", "fzaninotto/faker": "~1.4",

View File

@ -181,6 +181,7 @@ return [
// Barryvdh\Debugbar\ServiceProvider::class, // Barryvdh\Debugbar\ServiceProvider::class,
'DaveJamesMiller\Breadcrumbs\ServiceProvider', 'DaveJamesMiller\Breadcrumbs\ServiceProvider',
'TwigBridge\ServiceProvider', 'TwigBridge\ServiceProvider',
'PragmaRX\Google2FA\Vendor\Laravel\ServiceProvider',
], ],
@ -238,7 +239,7 @@ return [
'ExpandedForm' => 'FireflyIII\Support\Facades\ExpandedForm', 'ExpandedForm' => 'FireflyIII\Support\Facades\ExpandedForm',
'Entrust' => 'Zizaco\Entrust\EntrustFacade', 'Entrust' => 'Zizaco\Entrust\EntrustFacade',
'Input' => 'Illuminate\Support\Facades\Input', 'Input' => 'Illuminate\Support\Facades\Input',
'Google2FA' => 'PragmaRX\Google2FA\Vendor\Laravel\Facade',
], ],

View File

@ -230,6 +230,9 @@ return [
'pref_two_factor_auth' => '2-step verification', 'pref_two_factor_auth' => '2-step verification',
'pref_two_factor_auth_help' => 'When you enable 2-step verification (also known as two-factor authentication), you add an extra layer of security to your account. You sign in with something you know (your password) and something you have (a verification code). Verification codes are generated by an application on your phone, such as Authy or Google Authenticator.', 'pref_two_factor_auth_help' => 'When you enable 2-step verification (also known as two-factor authentication), you add an extra layer of security to your account. You sign in with something you know (your password) and something you have (a verification code). Verification codes are generated by an application on your phone, such as Authy or Google Authenticator.',
'pref_enable_two_factor_auth' => 'Enable 2-step verification', 'pref_enable_two_factor_auth' => 'Enable 2-step verification',
'pref_two_factor_auth_code' => 'Verify code',
'pref_two_factor_auth_code_help' => 'Scan the QR code with an application on your phone such as Authy or Google Authenticator and enter the generated code.',
'pref_two_factor_auth_reset_code' => 'Reset verification code',
'pref_save_settings' => 'Save settings', 'pref_save_settings' => 'Save settings',
// profile: // profile:

View File

@ -68,4 +68,5 @@ return [
'string' => 'The :attribute must be a string.', 'string' => 'The :attribute must be a string.',
'url' => 'The :attribute format is invalid.', 'url' => 'The :attribute format is invalid.',
'timezone' => 'The :attribute must be a valid zone.', 'timezone' => 'The :attribute must be a valid zone.',
'2fa_code' => 'The :attribute field is invalid.',
]; ];

View File

@ -228,6 +228,9 @@ return [
'pref_two_factor_auth' => 'Authenticatie in twee stappen', 'pref_two_factor_auth' => 'Authenticatie in twee stappen',
'pref_two_factor_auth_help' => 'Als je authenticatie in twee stappen (ook wel twee-factor authenticatie genoemd) inschakelt voeg je een extra beveiligingslaag toe aan je account. Je logt in met iets dat je weet (je wachtwoord) en iets dat je hebt (een verificatiecode). Verificatiecodes worden gegeneerd door apps op je telefoon, zoals Authy en Google Authenticator.', 'pref_two_factor_auth_help' => 'Als je authenticatie in twee stappen (ook wel twee-factor authenticatie genoemd) inschakelt voeg je een extra beveiligingslaag toe aan je account. Je logt in met iets dat je weet (je wachtwoord) en iets dat je hebt (een verificatiecode). Verificatiecodes worden gegeneerd door apps op je telefoon, zoals Authy en Google Authenticator.',
'pref_enable_two_factor_auth' => 'Authenticatie in twee stappen inschakelen', 'pref_enable_two_factor_auth' => 'Authenticatie in twee stappen inschakelen',
'pref_two_factor_auth_code' => 'Verifieer code',
'pref_two_factor_auth_code_help' => 'Scan onderstaande QR code met een app op je telefoon zoals Authy of Google Authenticator en vul de code die gegenereerd wordt in.',
'pref_two_factor_auth_reset_code' => 'Reset verificatiecode',
'pref_save_settings' => 'Instellingen opslaan', 'pref_save_settings' => 'Instellingen opslaan',
// profile: // profile:

View File

@ -68,4 +68,6 @@ return [
'string' => 'Het :attribute moet een tekenreeks zijn.', 'string' => 'Het :attribute moet een tekenreeks zijn.',
'url' => ':attribute is geen geldige URL.', 'url' => ':attribute is geen geldige URL.',
'timezone' => 'Het :attribute moet een geldige zone zijn.', 'timezone' => 'Het :attribute moet een geldige zone zijn.',
'2fa_code' => ':attribute is ongeldig.',
]; ];

View File

@ -0,0 +1,52 @@
{% extends "./layout/default.twig" %}
{% block breadcrumbs %}
{{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }}
{% endblock %}
{% block content %}
{{ Form.open({'class' : 'form-horizontal','id' : 'preferences.code'}) }}
<input type="hidden" name="secret" value="{{ secret }}"/>
<div class="row">
<div class="col-lg-6 col-md-6 col-sm-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'pref_two_factor_auth_code'|_ }}</h3>
</div>
<div class="box-body">
<p class="text-info">
{{ 'pref_two_factor_auth_code_help'|_ }}
</p>
<div class="form group">
<div class="col-sm-8 col-md-offset-4">
<img src="{{ image }}" />
</div>
</div>
<div class="form group">
<div class="col-sm-8 col-md-offset-4">
<p>{{ secret }}</p>
</div>
</div>
{{ ExpandedForm.text('code', code, {'label' : 'Code'}) }}
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<div class="form-group">
<div class="col-sm-12">
<button type="submit" class="btn btn-success btn-lg">{{ 'pref_save_settings'|_ }}</button>
</div>
</div>
</div>
</div>
<!-- form close -->
{{ Form.close|raw }}
{% endblock %}

View File

@ -145,19 +145,34 @@
</div> </div>
<div class="box-body"> <div class="box-body">
<p class="text-info">{{ 'pref_two_factor_auth_help'|_ }}</p> <p class="text-info">{{ 'pref_two_factor_auth_help'|_ }}</p>
<div class="form-group"> <div class="form-group">
<div class="col-sm-10"> <div class="col-sm-10">
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" name="twoFactorAuthEnabled" value="1" <input type="checkbox" name="twoFactorAuthEnabled" value="1"
{% if twoFactorAuthEnabled == '1' %} checked {% endif %}> {{ 'pref_enable_two_factor_auth'|_ }} {% if twoFactorAuthEnabled == '1' %} checked {% endif %}> {{ 'pref_enable_two_factor_auth'|_ }}
</label> </label>
</div>
</div> </div>
</div> </div>
{% if twoFactorAuthEnabled == '1' and hasTwoFactorAuthSecret == true %}
<div class="col-sm-10">
<div class="checkbox">
<label>
<a href="{{ URL.to('/preferences/code') }}">{{ 'pref_two_factor_auth_reset_code'|_ }}</a>
</label>
</div>
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-lg-12 col-md-12 col-sm-12"> <div class="col-lg-12 col-md-12 col-sm-12">
@ -171,4 +186,4 @@
<!-- form close --> <!-- form close -->
{{ Form.close|raw }} {{ Form.close|raw }}
{% endblock %} {% endblock %}