diff --git a/app/config/app.php b/app/config/app.php index 539138aa..8f8dd215 100644 --- a/app/config/app.php +++ b/app/config/app.php @@ -135,6 +135,7 @@ return [ 'Thujohn\Rss\RssServiceProvider', 'Jenssegers\Date\DateServiceProvider', 'McCool\LaravelAutoPresenter\LaravelAutoPresenterServiceProvider', + 'PragmaRX\Google2FA\Vendor\Laravel\ServiceProvider', /* * Application Service Providers... diff --git a/app/database/migrations/2015_01_09_083419_AlterTableUsersAdd2FA.php b/app/database/migrations/2015_01_09_083419_AlterTableUsersAdd2FA.php new file mode 100644 index 00000000..3e933caf --- /dev/null +++ b/app/database/migrations/2015_01_09_083419_AlterTableUsersAdd2FA.php @@ -0,0 +1,32 @@ +string('google_2fa_secret')->after('remember_token'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('google_2fa_secret'); + }); + } +} diff --git a/app/lang/en/dashboard.php b/app/lang/en/dashboard.php index c1c31aba..45963149 100644 --- a/app/lang/en/dashboard.php +++ b/app/lang/en/dashboard.php @@ -108,9 +108,10 @@ return [ // Login 'login' => [ - 'login' => 'Login', - 'logged_in' => "You're logged in.", - 'welcome' => 'Welcome Back!', + 'login' => 'Login', + 'logged_in' => 'You\'re logged in.', + 'welcome' => 'Welcome Back!', + 'two-factor' => 'Please enter your token.', ], // Sidebar footer diff --git a/app/lang/en/forms.php b/app/lang/en/forms.php index 705d2247..a7612b7a 100644 --- a/app/lang/en/forms.php +++ b/app/lang/en/forms.php @@ -4,19 +4,21 @@ return [ // Setup form fields 'setup' => [ - 'email' => 'Email', - 'username' => 'Username', - 'password' => 'Password', - 'site_name' => 'Site Name', - 'site_domain' => 'Site Domain', - 'site_timezone' => 'Select your timezone', - 'site_locale' => 'Select your language', + 'email' => 'Email', + 'username' => 'Username', + 'password' => 'Password', + 'site_name' => 'Site Name', + 'site_domain' => 'Site Domain', + 'site_timezone' => 'Select your timezone', + 'site_locale' => 'Select your language', + 'enable_google2fa' => 'Enable Google Two Factor Authentication', ], // Login form fields 'login' => [ 'email' => 'Email', 'password' => 'Password', + '2fauth' => 'Authentication Code', ], // Incidents form fields @@ -79,6 +81,9 @@ return [ 'password' => 'Password', 'api-key' => 'API Key', 'api-key-help' => 'Regenerating your API key will revoke all existing applications.', + '2fa' => [ + 'help' => 'Enabling two factor authentication increases security of your account. You will need to download Google Authenticator or a similar app on to your mobile device. When you login you will be asked to provide a token generated by the app.', + ], ], // Buttons diff --git a/app/lang/fr/dashboard.php b/app/lang/fr/dashboard.php index 74064660..a521b110 100644 --- a/app/lang/fr/dashboard.php +++ b/app/lang/fr/dashboard.php @@ -108,9 +108,10 @@ return [ // Login 'login' => [ - 'login' => 'Connexion', - 'logged_in' => "Vous êtes connecté.", - 'welcome' => 'Re-bonjour !', + 'login' => 'Connexion', + 'logged_in' => "Vous êtes connecté.", + 'welcome' => 'Re-bonjour !', + 'two-factor' => 'Please enter your token.', ], // Sidebar footer diff --git a/app/lang/fr/forms.php b/app/lang/fr/forms.php index 8c8fbdf9..3ed6b82f 100644 --- a/app/lang/fr/forms.php +++ b/app/lang/fr/forms.php @@ -4,19 +4,21 @@ return [ // Setup form fields 'setup' => [ - 'email' => 'Adresse email', - 'username' => 'Identifiant', - 'password' => 'Mot de passe', - 'site_name' => 'Nom du site', - 'site_domain' => 'Domaine du site', - 'site_timezone' => 'Sélectionnez votre fuseau horaire', - 'site_locale' => 'Sélectionnez votre langue', + 'email' => 'Adresse email', + 'username' => 'Identifiant', + 'password' => 'Mot de passe', + 'site_name' => 'Nom du site', + 'site_domain' => 'Domaine du site', + 'site_timezone' => 'Sélectionnez votre fuseau horaire', + 'site_locale' => 'Sélectionnez votre langue', + 'enable_google2fa' => 'Enable Google Two Factor Authentication', ], // Login form fields 'login' => [ 'email' => 'Adresse email', 'password' => 'Mot de passe', + '2fauth' => 'Authentication Code', ], // Incidents form fields @@ -79,6 +81,9 @@ return [ 'password' => 'Mot de passe', 'api-key' => 'Clé API', 'api-key-help' => 'Regénérer votre clé API révoquera toutes les applications existantes.', + '2fa' => [ + 'help' => 'Enabling two factor authentication increases security of your account. You will need to download Google Authenticator or a similar app on to your mobile device. When you login you will be asked to provide a token generated by the app.', + ], ], // Buttons diff --git a/app/lang/pt-BR/dashboard.php b/app/lang/pt-BR/dashboard.php index 82d22f97..6f474d50 100755 --- a/app/lang/pt-BR/dashboard.php +++ b/app/lang/pt-BR/dashboard.php @@ -1,6 +1,7 @@ 'Dashboard', // Incidents @@ -97,7 +98,7 @@ return [ 'stylesheet' => 'Folha de estilo', ], 'theme' => [ - 'theme' => ' Tema', + 'theme' => 'Tema', ], 'edit' => [ 'success' => 'Configurações salvas.', @@ -107,9 +108,10 @@ return [ // Login 'login' => [ - 'login' => 'Login', - 'logged_in' => "Você está logado.", - 'welcome' => 'Bem-vindo de volta!', + 'login' => 'Login', + 'logged_in' => "Você está logado.", + 'welcome' => 'Bem-vindo de volta!', + 'two-factor' => 'Please enter your token.', ], // Sidebar footer diff --git a/app/lang/pt-BR/forms.php b/app/lang/pt-BR/forms.php index 7b125e08..29db5137 100755 --- a/app/lang/pt-BR/forms.php +++ b/app/lang/pt-BR/forms.php @@ -3,19 +3,21 @@ return [ // Setup form fields 'setup' => [ - 'email' => 'Email', - 'username' => 'Usuário', - 'password' => 'Senha', - 'site_name' => 'Nome do site', - 'site_domain' => 'Domínio do site', - 'site_timezone' => 'Select your timezone', - 'site_locale' => 'Select your language', + 'email' => 'Email', + 'username' => 'Usuário', + 'password' => 'Senha', + 'site_name' => 'Nome do site', + 'site_domain' => 'Domínio do site', + 'site_timezone' => 'Select your timezone', + 'site_locale' => 'Select your language', + 'enable_google2fa' => 'Enable Google Two Factor Authentication', ], // Login form fields 'login' => [ 'email' => 'Email', 'password' => 'Senha', + '2fauth' => 'Authentication Code', ], // Incidents form fields @@ -78,6 +80,9 @@ return [ 'password' => 'Senha', 'api-key' => 'Chave da API', 'api-key-help' => 'Regenerar sua chave de API irá revogar todos os aplicativos existentes.', + '2fa' => [ + 'help' => 'Enabling two factor authentication increases security of your account. You will need to download Google Authenticator or a similar app on to your mobile device. When you login you will be asked to provide a token generated by the app.', + ], ], // Buttons diff --git a/app/routes/auth.php b/app/routes/auth.php index 1eeef9f1..2263566c 100644 --- a/app/routes/auth.php +++ b/app/routes/auth.php @@ -1,22 +1,33 @@ 'has_setting:app_name', 'namespace' => 'CachetHQ\Cachet\Http\Controllers'], function () { - // Login routes - Route::get('/auth/login', [ - 'before' => 'guest', - 'as' => 'login', - 'uses' => 'AuthController@showLogin', - ]); - Route::post('/auth/login', [ - 'before' => 'guest|csrf|login_throttling', - 'as' => 'logout', - 'uses' => 'AuthController@postLogin', - ]); -}); +Route::group(['prefix' => 'auth', 'namespace' => 'CachetHQ\Cachet\Http\Controllers'], function () { + Route::group(['before' => 'has_setting:app_name'], function () { + // Login routes + Route::get('login', [ + 'before' => 'guest', + 'as' => 'login', + 'uses' => 'AuthController@showLogin', + ]); -Route::group(['before' => 'auth', 'namespace' => 'CachetHQ\Cachet\Http\Controllers'], function () { - Route::get('/auth/logout', [ - 'as' => 'logout', - 'uses' => 'AuthController@logoutAction', - ]); + Route::post('login', [ + 'before' => 'guest|csrf|login_throttling', + 'as' => 'logout', + 'uses' => 'AuthController@postLogin', + ]); + + // Two factor authorization + Route::get('2fa', [ + 'as' => 'two-factor', + 'uses' => 'AuthController@showTwoFactorAuth', + ]); + + Route::post('2fa', 'AuthController@postTwoFactor'); + }); + + Route::group(['before' => 'auth'], function () { + Route::get('logout', [ + 'as' => 'logout', + 'uses' => 'AuthController@logoutAction', + ]); + }); }); diff --git a/app/views/auth/two-factor-auth.blade.php b/app/views/auth/two-factor-auth.blade.php new file mode 100644 index 00000000..95474428 --- /dev/null +++ b/app/views/auth/two-factor-auth.blade.php @@ -0,0 +1,31 @@ +@extends('layout.clean') + +@section('content') +
+
+ + {{ Form::open() }} +
+ {{ trans('dashboard.login.two-factor') }} + + @if(Session::has('error')) +

{{ Session::get('error') }}

+ @endif + +
+ + {{ Form::text('code', null, [ + 'class' => 'form-control', 'placeholder' => trans('forms.login.2fauth'), 'required' => 'required' + ]) }} +
+
+
+ +
+
+ {{ Form::close() }} +
+
+@stop diff --git a/app/views/dashboard/team/edit.blade.php b/app/views/dashboard/team/edit.blade.php index a753b5c4..7c867933 100644 --- a/app/views/dashboard/team/edit.blade.php +++ b/app/views/dashboard/team/edit.blade.php @@ -6,7 +6,7 @@ - {{ trans('dashboard.user.user') }} + {{ trans('dashboard.team.member') }}
@@ -15,9 +15,9 @@ @if($updated = Session::get('updated'))
@if($updated) - {{ sprintf("%s %s", trans('dashboard.notifications.awesome'), trans('dashboard.user.edit.success')) }} + {{ sprintf("%s %s", trans('dashboard.notifications.awesome'), trans('dashboard.team.edit.success')) }} @else - {{ sprintf("%s %s", trans('dashboard.notifications.whoops'), trans('dashboard.user.edit.failure')) }} + {{ sprintf("%s %s", trans('dashboard.notifications.whoops'), trans('dashboard.team.edit.failure')) }} @endif
@endif diff --git a/app/views/dashboard/team/index.blade.php b/app/views/dashboard/team/index.blade.php index a9a6cf6e..ba62e098 100644 --- a/app/views/dashboard/team/index.blade.php +++ b/app/views/dashboard/team/index.blade.php @@ -21,7 +21,7 @@
@foreach($teamMembers as $member)
- +
{{ $member->username }}
diff --git a/app/views/dashboard/user/index.blade.php b/app/views/dashboard/user/index.blade.php index c284c239..c9c7026f 100644 --- a/app/views/dashboard/user/index.blade.php +++ b/app/views/dashboard/user/index.blade.php @@ -15,9 +15,9 @@ @if($updated = Session::get('updated'))
@if($updated) - {{ sprintf("%s %s", trans('dashboard.notifications.awesome'), trans('dashboard.user.edit.success')) }} + {{ sprintf("%s %s", trans('dashboard.notifications.awesome'), trans('dashboard.team.edit.success')) }} @else - {{ sprintf("%s %s", trans('dashboard.notifications.whoops'), trans('dashboard.user.edit.failure')) }} + {{ sprintf("%s %s", trans('dashboard.notifications.whoops'), trans('dashboard.team.edit.failure')) }} @endif
@endif @@ -42,6 +42,27 @@ {{ trans('forms.user.api-key-help') }}
+
+
+ +
+ @if(Auth::user()->hasTwoFactor) +
+ email, + Auth::user()->google_2fa_secret + ); + ?> + + {{ trans('forms.user.2fa.help') }} +
+ @endif diff --git a/composer.json b/composer.json index bbc971c6..86394b5d 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "thujohn/rss": "~1.0", "watson/validating": "0.10.*", "jenssegers/date": "~2.0", - "mccool/laravel-auto-presenter": "~2.2@dev" + "mccool/laravel-auto-presenter": "~2.2@dev", + "pragmarx/google2fa": "~0.1.0" }, "require-dev": { "phpunit/phpunit": "~4.3", diff --git a/composer.lock b/composer.lock index fcc65d29..4f915d11 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "b561fa017bd12ef775f7765fe28d8839", + "hash": "54919d71db11cf852620b58cd12a982b", "packages": [ { "name": "classpreloader/classpreloader", @@ -1829,6 +1829,58 @@ ], "time": "2014-11-10 03:08:59" }, + { + "name": "pragmarx/google2fa", + "version": "v0.1.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "705c2ff3e59212636fdfe31405e0e667a4f31514" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/705c2ff3e59212636fdfe31405e0e667a4f31514", + "reference": "705c2ff3e59212636fdfe31405e0e667a4f31514", + "shasum": "" + }, + "require": { + "php": ">=5.3.7" + }, + "require-dev": { + "phpspec/phpspec": "~2.1@dev" + }, + "type": "library", + "extra": { + "component": "package", + "frameworks": [ + "Laravel" + ] + }, + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "Authentication", + "Two Factor Authentication", + "google2fa", + "laravel" + ], + "time": "2014-09-22 15:53:31" + }, { "name": "predis/predis", "version": "v0.8.7", diff --git a/src/Http/Controllers/AuthController.php b/src/Http/Controllers/AuthController.php index fe5ace84..779d35f8 100644 --- a/src/Http/Controllers/AuthController.php +++ b/src/Http/Controllers/AuthController.php @@ -8,7 +8,9 @@ use Illuminate\Routing\Controller; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Redirect; use Illuminate\Support\Facades\Request; +use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\View; +use PragmaRX\Google2FA\Vendor\Laravel\Facade as Google2FA; /** * Logs users into their account. @@ -32,7 +34,22 @@ class AuthController extends Controller */ public function postLogin() { - if (Auth::attempt(Binput::only(['email', 'password']))) { + $loginData = Binput::only(['email', 'password']); + // Validate login credentials. + if (Auth::validate($loginData)) { + // Log the user in for one request. + Auth::once($loginData); + // Do we have Two Factor Auth enabled? + if (Auth::user()->hasTwoFactor) { + // Temporarily store the user. + Session::put('2fa_id', Auth::user()->id); + + return Redirect::route('two-factor'); + } + + // We probably wan't to add support for "Remember me" here. + Auth::attempt(Binput::only(['email', 'password'])); + return Redirect::intended('dashboard'); } @@ -43,6 +60,47 @@ class AuthController extends Controller ->with('error', 'Invalid email or password'); } + /** + * Shows the two-factor-auth view. + * + * @return \Illuminate\View\View + */ + public function showTwoFactorAuth() + { + return View::make('auth.two-factor-auth'); + } + + /** + * Validates the Two Factor token. + * + * This feels very hacky, but we have to juggle authentication and codes. + * + * @return \Illuminate\Http\RedirectResponse + */ + public function postTwoFactor() + { + // Check that we have a session. + if ($userId = Session::pull('2fa_id')) { + $code = Binput::get('code'); + + // Maybe a temp login here. + Auth::loginUsingId($userId); + + $valid = Google2FA::verifyKey(Auth::user()->google_2fa_secret, $code); + + if ($valid) { + return Redirect::intended('dashboard'); + } else { + // Failed login, log back out. + Auth::logout(); + + return Redirect::route('login')->with('error', 'Invalid token'); + } + } + + return Redirect::route('login')->with('error', 'Invalid token'); + } + /** * Logs the user out, deleting their session etc. * diff --git a/src/Http/Controllers/DashUserController.php b/src/Http/Controllers/DashUserController.php index 9692de04..59c49826 100644 --- a/src/Http/Controllers/DashUserController.php +++ b/src/Http/Controllers/DashUserController.php @@ -8,6 +8,7 @@ use Illuminate\Routing\Controller; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Redirect; use Illuminate\Support\Facades\View; +use PragmaRX\Google2FA\Vendor\Laravel\Facade as Google2FA; class DashUserController extends Controller { @@ -32,6 +33,20 @@ class DashUserController extends Controller { $items = Binput::all(); + $passwordChange = array_get($items, 'password'); + $enable2FA = (bool) array_pull($items, 'google2fa'); + + // Let's enable/disable auth + if ($enable2FA && ! Auth::user()->hasTwoFactor) { + $items['google_2fa_secret'] = Google2FA::generateSecretKey(); + } elseif (! $enable2FA) { + $items['google_2fa_secret'] = ''; + } + + if (trim($passwordChange) === '') { + unset($items['password']); + } + $updated = Auth::user()->update($items); return Redirect::back()->with('updated', $updated); diff --git a/src/Http/Controllers/SetupController.php b/src/Http/Controllers/SetupController.php index 2ac4f043..e74a87ec 100644 --- a/src/Http/Controllers/SetupController.php +++ b/src/Http/Controllers/SetupController.php @@ -126,7 +126,6 @@ class SetupController extends Controller // Pull the user details out. $userDetails = array_pull($postData, 'user'); - // TODO: Do we want to just use Model::unguard() here? $user = User::create([ 'username' => $userDetails['username'], 'email' => $userDetails['email'], diff --git a/src/Models/User.php b/src/Models/User.php index 59917a8d..0cfbaca0 100644 --- a/src/Models/User.php +++ b/src/Models/User.php @@ -44,7 +44,7 @@ class User extends Model implements UserInterface, RemindableInterface * * @var string[] */ - protected $hidden = ['password', 'remember_token']; + protected $hidden = ['password', 'remember_token', 'google_2fa_secret']; /** * The properties that cannot be mass assigned. @@ -133,4 +133,14 @@ class User extends Model implements UserInterface, RemindableInterface { return (bool) $this->level; } + + /** + * Returns if a user has enabled two factor authentication. + * + * @return bool + */ + public function getHasTwoFactorAttribute() + { + return trim($this->google_2fa_secret) !== ''; + } }