diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b817577 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.idea +/vendor +/node_modules +package-lock.json +composer.phar +composer.lock +phpunit.xml +.phpunit.result.cache +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..f87f5c1 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# TODO \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9266a05 --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "lifeonscreen/nova-google2fa", + "description": "This package provides Google2FA to Laravel Nova.", + "keywords": [ + "laravel", + "nova" + ], + "license": "MIT", + "require": { + "php": ">=7.1.0", + "bacon/bacon-qr-code": "^2.0", + "pragmarx/google2fa-laravel": "^0.2.0" + }, + "autoload": { + "psr-4": { + "Lifeonscreen\\Google2fa\\": "src/" + } + }, + "extra": { + "laravel": { + "providers": [ + "Lifeonscreen\\Google2fa\\ToolServiceProvider" + ] + } + }, + "config": { + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/lifeonscreen2fa.php b/config/lifeonscreen2fa.php new file mode 100644 index 0000000..f6c00ec --- /dev/null +++ b/config/lifeonscreen2fa.php @@ -0,0 +1,10 @@ +<?php + +return [ + 'models' => [ + 'user' => 'App\User', + ], + 'tables' => [ + 'user' => 'users', + ], +]; \ No newline at end of file diff --git a/resources/database/2018_10_15_095425_create_user_2fa_table.php b/resources/database/2018_10_15_095425_create_user_2fa_table.php new file mode 100644 index 0000000..0adb7dd --- /dev/null +++ b/resources/database/2018_10_15_095425_create_user_2fa_table.php @@ -0,0 +1,41 @@ +<?php + +use Illuminate\Support\Facades\Schema; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Migrations\Migration; + +class CreateUser2faTable extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::create('user_2fa', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('user_id'); + $table->boolean('google2fa_enable')->default(false); + $table->string('google2fa_secret')->nullable(); + $table->timestamp('created_at') + ->default(DB::raw('CURRENT_TIMESTAMP')); + $table->timestamp('updated_at') + ->default(DB::raw('CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP')); + + $table->foreign('user_id') + ->references('id') + ->on(config('lifeonscreen2fa.tables.user')); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('user_2fa'); + } +} \ No newline at end of file diff --git a/resources/views/authenticate.blade.php b/resources/views/authenticate.blade.php new file mode 100644 index 0000000..4080b5f --- /dev/null +++ b/resources/views/authenticate.blade.php @@ -0,0 +1,63 @@ +<!DOCTYPE html> +<html lang="en" class="h-full font-sans"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="csrf-token" content="{{ csrf_token() }}"> + + <title>{{ Nova::name() }}</title> + + <!-- Styles --> + <link rel="stylesheet" href="{{ mix('app.css', 'vendor/nova') }}"> + + <style> + body { + font-family: "Montserrat", sans-serif !important; + } + + .btn, + .form-input, + .rounded-lg { + border-radius: 0 !important; + } + </style> +</head> +<body class="bg-40 text-black h-full"> +<div class="h-full"> + <div class="px-view py-view mx-auto"> + <div class="mx-auto py-8 max-w-sm text-center text-90"> + @include('nova::partials.logo') + </div> + + <form class="bg-white shadow rounded-lg p-8 max-w-xl mx-auto" method="POST" action="/los/2fa/authenticate"> + <h2 class="p-2">Two Factor Authentication</h2> + + <p class="p-2">Two factor authentication (2FA) strengthens access security by requiring two methods (also + referred to as factors) to + verify your identity. + Two factor authentication protects against phishing, social engineering and password brute force attacks + and secures your logins from attackers + exploiting weak or stolen credentials.</p> + <p class="p-2"><strong>Enter the pin from Google Authenticator Enable 2FA</strong></p> + + <div class="text-center pt-3"> + <div class="mb-6 w-1/2" style="display:inline-block"> + @if (isset($error)) + <p class="text-center font-semibold text-danger my-3"> + {{ $error }} + </p> + @endif + <label class="block font-bold mb-2" for="co">One Time Password</label> + <input class="form-control form-input form-input-bordered w-full" id="secret" type="number" + name="secret" value="" required="required" autofocus=""> + + </div> + <button class="w-1/2 btn btn-default btn-primary hover:bg-primary-dark" type="submit"> + Authenticate + </button> + </div> + </form> + </div> +</div> +</body> +</html> diff --git a/resources/views/register.blade.php b/resources/views/register.blade.php new file mode 100644 index 0000000..f33494d --- /dev/null +++ b/resources/views/register.blade.php @@ -0,0 +1,71 @@ +<!DOCTYPE html> +<html lang="en" class="h-full font-sans"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta name="csrf-token" content="{{ csrf_token() }}"> + + <title>{{ Nova::name() }}</title> + + <!-- Styles --> + <link rel="stylesheet" href="{{ mix('app.css', 'vendor/nova') }}"> + + <style> + body { + font-family: "Montserrat", sans-serif !important; + } + + .btn, + .form-input, + .rounded-lg { + border-radius: 0 !important; + } + </style> +</head> +<body class="bg-40 text-black h-full"> +<div class="h-full"> + <div class="px-view py-view mx-auto"> + <div class="mx-auto py-8 max-w-sm text-center text-90"> + @include('nova::partials.logo') + </div> + + <form class="bg-white shadow rounded-lg p-8 max-w-xl mx-auto" method="POST" action="/los/2fa/confirm"> + <h2 class="p-2">Two Factor Authentication</h2> + + <p class="p-2">Two factor authentication (2FA) strengthens access security by requiring two methods (also + referred + to as factors) to verify your identity. Two factor authentication protects against phishing, social + engineering and password brute force attacks and secures your logins from attackers exploiting weak + or stolen credentials.</p> + <p class="p-2">To Enable Two Factor Authentication on your Account, you need to do following steps</p> + <strong> + <ol> + <li>Verify the OTP from Google Authenticator Mobile App</li> + </ol> + </strong> + <div class="text-center"> + <img src="{{ $google2fa_url }}" alt=""> + </div> + + <div class="text-center"> + <div class="mb-6 w-1/2" style="display:inline-block"> + @if (isset($error)) + <p class="text-center font-semibold text-danger my-3"> + {{ $error }} + </p> + @endif + <label class="block font-bold mb-2" for="co">Secret</label> + <input class="form-control form-input form-input-bordered w-full" id="secret" type="number" + name="secret" value="" required="required" autofocus=""> + </div> + + + <button class="w-1/2 btn btn-default btn-primary hover:bg-primary-dark" type="submit"> + Confirm + </button> + </div> + </form> + </div> +</div> +</body> +</html> diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..c2e1275 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,13 @@ +<?php + +use Illuminate\Support\Facades\Route; + +/** + * This route is called when user must first time confirm secret + */ +Route::post('confirm', 'Lifeonscreen\Google2fa\Google2fa@confirm'); + +/** + * This route is called to verify users secret + */ +Route::post('authenticate', 'Lifeonscreen\Google2fa\Google2fa@authenticate'); \ No newline at end of file diff --git a/src/Google2FAAuthenticator.php b/src/Google2FAAuthenticator.php new file mode 100644 index 0000000..cf76486 --- /dev/null +++ b/src/Google2FAAuthenticator.php @@ -0,0 +1,35 @@ +<?php + +namespace Lifeonscreen\Google2fa; + +use App\Exceptions\ValidationException; +use PragmaRX\Google2FALaravel\Support\Authenticator; + +/** + * Class Google2FAAuthenticator + * @package Lifeonscreen\Google2fa + */ +class Google2FAAuthenticator extends Authenticator +{ + protected function canPassWithoutCheckingOTP() + { + return + !$this->isEnabled() || + $this->noUserIsAuthenticated() || + $this->twoFactorAuthStillValid(); + } + + /** + * @return mixed + * @throws ValidationException + */ + protected function getGoogle2FASecretKey() + { + $secret = $this->getUser()->user2fa->{$this->config('otp_secret_column')}; + if (is_null($secret) || empty($secret)) { + throw new ValidationException('Secret key cannot be empty.'); + } + + return $secret; + } +} \ No newline at end of file diff --git a/src/Google2fa.php b/src/Google2fa.php new file mode 100644 index 0000000..243f0ca --- /dev/null +++ b/src/Google2fa.php @@ -0,0 +1,80 @@ +<?php + +namespace Lifeonscreen\Google2fa; + +use Laravel\Nova\Tool; +use PragmaRX\Google2FA\Google2FA as G2fa; +use Request; + +class Google2fa extends Tool +{ + /** + * Perform any tasks that need to happen when the tool is booted. + * + * @return void + */ + public function boot() + { + } + + /** + * Build the view that renders the navigation links for the tool. + * + * @return \Illuminate\View\View + */ + public function renderNavigation() + { + return view('google2fa::navigation'); + } + + protected function is2FAValid() + { + $secret = Request::get('secret'); + + $google2fa = new G2fa(); + $google2fa->setAllowInsecureCallToGoogleApis(true); + + return $google2fa->verifyKey(auth()->user()->user2fa->google2fa_secret, $secret); + } + + public function confirm() + { + if ($this->is2FAValid()) { + auth()->user()->user2fa->google2fa_enable = 1; + auth()->user()->user2fa->save(); + $authenticator = app(Google2FAAuthenticator::class); + $authenticator->login(); + + return response()->redirectTo('/nova'); + } + + $google2fa = new G2fa(); + $secretKey = $google2fa->generateSecretKey(); + $google2fa->setAllowInsecureCallToGoogleApis(true); + + $google2fa_url = $google2fa->getQRCodeGoogleUrl( + config('app.name'), + auth()->user()->email, + $secretKey + ); + + $data['google2fa_url'] = $google2fa_url; + $data['error'] = 'Secret is invalid.'; + + return view('google2fa::register', $data); + + } + + public function authenticate() + { + if ($this->is2FAValid()) { + $authenticator = app(Google2FAAuthenticator::class); + $authenticator->login(); + + return response()->redirectTo('/nova'); + } + $data['error'] = 'One time password is invalid.'; + + return view('google2fa::authenticate', $data); + } +} diff --git a/src/Http/Middleware/Authorize.php b/src/Http/Middleware/Authorize.php new file mode 100644 index 0000000..969b152 --- /dev/null +++ b/src/Http/Middleware/Authorize.php @@ -0,0 +1,20 @@ +<?php + +namespace Lifeonscreen\Google2fa\Http\Middleware; + +use Lifeonscreen\Google2fa\Google2fa; + +class Authorize +{ + /** + * Handle the incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return \Illuminate\Http\Response + */ + public function handle($request, $next) + { + return resolve(Google2fa::class)->authorize($request) ? $next($request) : abort(403); + } +} diff --git a/src/Http/Middleware/Google2fa.php b/src/Http/Middleware/Google2fa.php new file mode 100644 index 0000000..980dd90 --- /dev/null +++ b/src/Http/Middleware/Google2fa.php @@ -0,0 +1,57 @@ +<?php + +namespace Lifeonscreen\Google2fa\Http\Middleware; + +use Lifeonscreen\Google2fa\Models\User2fa; +use Closure; +use Lifeonscreen\Google2fa\Google2FAAuthenticator; +use PragmaRX\Google2FA\Google2FA as G2fa; + +/** + * Class Google2fa + * @package Lifeonscreen\Google2fa\Http\Middleware + */ +class Google2fa +{ + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + * @throws \PragmaRX\Google2FA\Exceptions\InsecureCallException + */ + public function handle($request, Closure $next) + { + if ($request->path() == 'los/2fa/confirm' || $request->path() == 'los/2fa/authenticate') { + return $next($request); + } + $authenticator = app(Google2FAAuthenticator::class)->boot($request); + if (auth()->guest() || $authenticator->isAuthenticated()) { + return $next($request); + } + if (empty(auth()->user()->user2fa) || auth()->user()->user2fa->google2fa_enable === 0) { + + $google2fa = new G2fa(); + $secretKey = $google2fa->generateSecretKey(); + $google2fa->setAllowInsecureCallToGoogleApis(true); + + $google2fa_url = $google2fa->getQRCodeGoogleUrl( + config('app.name'), + auth()->user()->email, + $secretKey + ); + + $data['google2fa_url'] = $google2fa_url; + User2fa::where('user_id', auth()->user()->id)->delete(); + User2fa::insert([ + 'user_id' => auth()->user()->id, + 'google2fa_secret' => $secretKey, + ]); + + return response(view('google2fa::register', $data)); + } + + return response(view('google2fa::authenticate')); + } +} \ No newline at end of file diff --git a/src/Models/User2fa.php b/src/Models/User2fa.php new file mode 100644 index 0000000..0d2fb67 --- /dev/null +++ b/src/Models/User2fa.php @@ -0,0 +1,28 @@ +<?php + +namespace Lifeonscreen\Google2fa\Models; + +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; + +/** + * Class PasswordSecurity + * @package App\GraphQL\Models\User + */ +class User2fa extends Model +{ + /** + * @var string + */ + protected $table = 'user_2fa'; + + protected $guarded = []; + + /** + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo(config('lifeonscreen2fa.models.user')); + } +} \ No newline at end of file diff --git a/src/ToolServiceProvider.php b/src/ToolServiceProvider.php new file mode 100644 index 0000000..44ed367 --- /dev/null +++ b/src/ToolServiceProvider.php @@ -0,0 +1,59 @@ +<?php + +namespace Lifeonscreen\Google2fa; + +use Illuminate\Support\Facades\Route; +use Illuminate\Support\ServiceProvider; +use Lifeonscreen\Google2fa\Http\Middleware\Authorize; + +class ToolServiceProvider extends ServiceProvider +{ + /** + * Bootstrap any application services. + * + * @return void + */ + public function boot() + { + $this->loadViewsFrom(__DIR__ . '/../resources/views', 'google2fa'); + $this->loadMigrationsFrom(__DIR__ . '/../resources/database'); + + // Publishing is only necessary when using the CLI. + if ($this->app->runningInConsole()) { + // Publishing the configuration file. + $this->publishes([ + __DIR__ . '/../config/lifeonscreen2fa.php' => config_path('lifeonscreen2fa.php'), + ], 'lifeonscreen2fa.config'); + } + + $this->app->booted(function () { + $this->routes(); + }); + } + + /** + * Register the tool's routes. + * + * @return void + */ + protected function routes() + { + if ($this->app->routesAreCached()) { + return; + } + + Route::middleware(['nova', Authorize::class]) + ->prefix('los/2fa') + ->group(__DIR__ . '/../routes/api.php'); + } + + /** + * Register any application services. + * + * @return void + */ + public function register() + { + $this->mergeConfigFrom(__DIR__ . '/../config/lifeonscreen2fa.php', 'lifeonscreen2fa'); + } +}