Add two factor authentication

This commit is contained in:
Miguel Nogueira 2020-07-17 22:44:10 +01:00
parent 5f1f92a9ce
commit d392c0593f
24 changed files with 1010 additions and 21 deletions

View File

@ -0,0 +1,16 @@
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Traits\AuthenticatesTwoFactor;
class TwofaController extends Controller
{
use AuthenticatesTwoFactor;
protected $redirectTo = '/dashboard';
}

View File

@ -8,6 +8,8 @@ use App\Http\Requests\FlushSessionsRequest;
use App\Http\Requests\DeleteUserRequest; use App\Http\Requests\DeleteUserRequest;
use App\Http\Requests\SearchPlayerRequest; use App\Http\Requests\SearchPlayerRequest;
use App\Http\Requests\UpdateUserRequest; use App\Http\Requests\UpdateUserRequest;
use App\Http\Requests\Add2FASecretRequest;
use App\Http\Requests\Remove2FASecretRequest;
use App\User; use App\User;
use App\Ban; use App\Ban;
@ -21,6 +23,8 @@ use App\Notifications\EmailChanged;
use App\Notifications\ChangedPassword; use App\Notifications\ChangedPassword;
use Spatie\Permission\Models\Role; use Spatie\Permission\Models\Role;
use Google2FA;
class UserController extends Controller class UserController extends Controller
{ {
@ -112,10 +116,32 @@ class UserController extends Controller
} }
} }
public function showAccount() public function showAccount(Request $request)
{ {
$QRCode = null;
if (!$request->user()->has2FA())
{
if ($request->session()->has('twofaAttemptFailed'))
{
$twoFactorSecret = $request->session()->get('current2FA');
}
else
{
$twoFactorSecret = Google2FA::generateSecretKey(32, '');
$request->session()->put('current2FA', $twoFactorSecret);
}
$QRCode = Google2FA::getQRCodeInline(
config('app.name'),
$request->user()->email,
$twoFactorSecret
);
}
return view('dashboard.user.profile.useraccount') return view('dashboard.user.profile.useraccount')
->with('ip', request()->ip()); ->with('ip', request()->ip())
->with('twofaQRCode', $QRCode);
} }
@ -247,10 +273,67 @@ class UserController extends Controller
} }
public function add2FASecret(Add2FASecretRequest $request)
{
$currentSecret = $request->session()->get('current2FA');
$isValid = Google2FA::verifyKey($currentSecret, $request->otp);
if ($isValid)
{
$request->user()->twofa_secret = $currentSecret;
$request->user()->save();
Log::warning('SECURITY: User activated two-factor authentication', [
'initiator' => $request->user()->email,
'ip' => $request->ip()
]);
Google2FA::login();
Log::warning('SECURITY: Started two factor session automatically', [
'initiator' => $request->user()->email,
'ip' => $request->ip()
]);
$request->session()->forget('current2FA');
if ($request->session()->has('twofaAttemptFailed'))
$request->session()->forget('twofaAttemptFailed');
$request->session()->flash('success', '2FA succesfully enabled! You\'ll now be prompted for an OTP each time you log in.');
}
else
{
$request->session()->flash('error', 'Incorrect code. Please reopen the 2FA settings panel and try again.');
$request->session()->put('twofaAttemptFailed', true);
}
return redirect()->back();
}
public function remove2FASecret(Remove2FASecretRequest $request)
{
Log::warning('SECURITY: Disabling two factor authentication (user initiated)', [
'initiator' => $request->user()->email,
'ip' => $request->ip()
]);
$request->user()->twofa_secret = null;
$request->user()->save();
$request->session()->flash('success', 'Two-factor authentication disabled.');
return redirect()->back();
}
public function terminate(Request $request, User $user) public function terminate(Request $request, User $user)
{ {
$this->authorize('terminate', User::class); $this->authorize('terminate', User::class);
// TODO: move logic to policy
if (!$user->isStaffMember() || $user->is(Auth::user())) if (!$user->isStaffMember() || $user->is(Auth::user()))
{ {
$request->session()->flash('error', 'You cannot terminate this user.'); $request->session()->flash('error', 'You cannot terminate this user.');

View File

@ -64,6 +64,7 @@ class Kernel extends HttpKernel
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'eligibility' => \App\Http\Middleware\ApplicationEligibility::class, 'eligibility' => \App\Http\Middleware\ApplicationEligibility::class,
'usernameUUID' => \App\Http\Middleware\UsernameUUID::class, 'usernameUUID' => \App\Http\Middleware\UsernameUUID::class,
'forcelogout' => \App\Http\Middleware\ForceLogoutMiddleware::class 'forcelogout' => \App\Http\Middleware\ForceLogoutMiddleware::class,
'2fa' => \PragmaRX\Google2FALaravel\Middleware::class,
]; ];
} }

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class Add2FASecretRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
// current logic only updates currently authenticated user
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'otp' => 'required|string|min:6|max:6'
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class Remove2FASecretRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'currentPassword' => 'required|password',
'consent' => 'required|accepted'
];
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Traits;
use Google2FA;
use App\Http\Requests\Add2FASecretRequest;
use Illuminate\Support\Facades\Log;
trait AuthenticatesTwoFactor
{
public function verify2FA(Add2FASecretRequest $request)
{
$isValid = Google2FA::verifyKey($request->user()->twofa_secret, $request->otp);
if ($isValid)
{
Google2FA::login();
Log::info('SECURITY (postauth): One-time password verification succeeded', [
'initiator' => $request->user()->email,
'ip' => $request->ip()
]);
return redirect()->to($this->redirectTo);
}
else
{
Log::warning('SECURITY (preauth): One-time password verification failed', [
'initiator' => $request->user()->email,
'ip' => $request->ip()
]);
$request->session()->flash('error', 'Your one time password is invalid.');
return redirect()->back();
}
}
}

View File

@ -68,18 +68,24 @@ class User extends Authenticatable
} }
public function isBanned() public function isBanned()
{ {
return !$this->bans()->get()->isEmpty(); return !$this->bans()->get()->isEmpty();
} }
public function isStaffMember() public function isStaffMember()
{ {
return $this->hasAnyRole('reviewer', 'admin', 'hiringManager'); return $this->hasAnyRole('reviewer', 'admin', 'hiringManager');
} }
public function has2FA()
{
return !is_null($this->twofa_secret);
}
public function routeNotificationForSlack($notification) public function routeNotificationForSlack($notification)

View File

@ -22,6 +22,7 @@
"laravel/slack-notification-channel": "^2.0", "laravel/slack-notification-channel": "^2.0",
"laravel/tinker": "^2.0", "laravel/tinker": "^2.0",
"laravel/ui": "^2.0", "laravel/ui": "^2.0",
"pragmarx/google2fa-laravel": "^1.3",
"sentry/sentry-laravel": "1.7.1", "sentry/sentry-laravel": "1.7.1",
"spatie/laravel-permission": "^3.13" "spatie/laravel-permission": "^3.13"
}, },

332
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "51429857899e8134bbe6b4fa61145cc3", "content-hash": "7bd6f57124bb77e0b496e2200f729267",
"packages": [ "packages": [
{ {
"name": "almasaeed2010/adminlte", "name": "almasaeed2010/adminlte",
@ -221,6 +221,55 @@
], ],
"time": "2019-12-24T22:41:47+00:00" "time": "2019-12-24T22:41:47+00:00"
}, },
{
"name": "bacon/bacon-qr-code",
"version": "2.0.1",
"source": {
"type": "git",
"url": "https://github.com/Bacon/BaconQrCode.git",
"reference": "6e53ced3d2499cee4a0ef23a7c4d6449607ac7da"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/6e53ced3d2499cee4a0ef23a7c4d6449607ac7da",
"reference": "6e53ced3d2499cee4a0ef23a7c4d6449607ac7da",
"shasum": ""
},
"require": {
"dasprid/enum": "^1.0",
"ext-iconv": "*",
"php": "^7.1"
},
"require-dev": {
"phly/keep-a-changelog": "^1.4",
"phpunit/phpunit": "^6.4",
"squizlabs/php_codesniffer": "^3.1"
},
"suggest": {
"ext-imagick": "to generate QR code images"
},
"type": "library",
"autoload": {
"psr-4": {
"BaconQrCode\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "http://www.dasprids.de",
"role": "Developer"
}
],
"description": "BaconQrCode is a QR code generator for PHP.",
"homepage": "https://github.com/Bacon/BaconQrCode",
"time": "2020-07-14T11:04:05+00:00"
},
{ {
"name": "brick/math", "name": "brick/math",
"version": "0.8.15", "version": "0.8.15",
@ -374,6 +423,48 @@
"description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)",
"time": "2020-04-23T11:49:37+00:00" "time": "2020-04-23T11:49:37+00:00"
}, },
{
"name": "dasprid/enum",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/DASPRiD/Enum.git",
"reference": "631ef6e638e9494b0310837fa531bedd908fc22b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/DASPRiD/Enum/zipball/631ef6e638e9494b0310837fa531bedd908fc22b",
"reference": "631ef6e638e9494b0310837fa531bedd908fc22b",
"shasum": ""
},
"require-dev": {
"phpunit/phpunit": "^6.4",
"squizlabs/php_codesniffer": "^3.1"
},
"type": "library",
"autoload": {
"psr-4": {
"DASPRiD\\Enum\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-2-Clause"
],
"authors": [
{
"name": "Ben Scholzen 'DASPRiD'",
"email": "mail@dasprids.de",
"homepage": "https://dasprids.de/"
}
],
"description": "PHP 7.1 enum implementation",
"keywords": [
"enum",
"map"
],
"time": "2017-10-25T22:45:27+00:00"
},
{ {
"name": "dnoegel/php-xdg-base-dir", "name": "dnoegel/php-xdg-base-dir",
"version": "v0.1.1", "version": "v0.1.1",
@ -2260,6 +2351,68 @@
], ],
"time": "2020-05-25T09:32:45+00:00" "time": "2020-05-25T09:32:45+00:00"
}, },
{
"name": "paragonie/constant_time_encoding",
"version": "v2.3.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "47a1cedd2e4d52688eb8c96469c05ebc8fd28fa2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/47a1cedd2e4d52688eb8c96469c05ebc8fd28fa2",
"reference": "47a1cedd2e4d52688eb8c96469c05ebc8fd28fa2",
"shasum": ""
},
"require": {
"php": "^7|^8"
},
"require-dev": {
"phpunit/phpunit": "^6|^7",
"vimeo/psalm": "^1|^2|^3"
},
"type": "library",
"autoload": {
"psr-4": {
"ParagonIE\\ConstantTime\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com",
"role": "Maintainer"
},
{
"name": "Steve 'Sc00bz' Thomas",
"email": "steve@tobtu.com",
"homepage": "https://www.tobtu.com",
"role": "Original Developer"
}
],
"description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
"keywords": [
"base16",
"base32",
"base32_decode",
"base32_encode",
"base64",
"base64_decode",
"base64_encode",
"bin2hex",
"encoding",
"hex",
"hex2bin",
"rfc4648"
],
"time": "2019-11-06T19:20:29+00:00"
},
{ {
"name": "paragonie/random_compat", "name": "paragonie/random_compat",
"version": "v9.99.99", "version": "v9.99.99",
@ -2783,6 +2936,183 @@
], ],
"time": "2020-03-21T18:07:53+00:00" "time": "2020-03-21T18:07:53+00:00"
}, },
{
"name": "pragmarx/google2fa",
"version": "8.0.0",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/google2fa.git",
"reference": "26c4c5cf30a2844ba121760fd7301f8ad240100b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/26c4c5cf30a2844ba121760fd7301f8ad240100b",
"reference": "26c4c5cf30a2844ba121760fd7301f8ad240100b",
"shasum": ""
},
"require": {
"paragonie/constant_time_encoding": "^1.0|^2.0",
"php": "^7.1|^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.18",
"phpunit/phpunit": "^7.5.15|^8.5|^9.0"
},
"type": "library",
"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": [
"2fa",
"Authentication",
"Two Factor Authentication",
"google2fa"
],
"time": "2020-04-05T10:47:18+00:00"
},
{
"name": "pragmarx/google2fa-laravel",
"version": "v1.3.3",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/google2fa-laravel.git",
"reference": "ed6e0a9ea1519550688ffb5afb4919204e46ecea"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/google2fa-laravel/zipball/ed6e0a9ea1519550688ffb5afb4919204e46ecea",
"reference": "ed6e0a9ea1519550688ffb5afb4919204e46ecea",
"shasum": ""
},
"require": {
"laravel/framework": ">=5.4.36",
"php": ">=7.0",
"pragmarx/google2fa-qrcode": "^1.0"
},
"require-dev": {
"orchestra/testbench": "3.4.*|3.5.*|3.6.*|3.7.*|4.*",
"phpunit/phpunit": "~5|~6|~7|~8"
},
"suggest": {
"bacon/bacon-qr-code": "Required to generate inline QR Codes.",
"pragmarx/recovery": "Generate recovery codes."
},
"type": "library",
"extra": {
"component": "package",
"frameworks": [
"Laravel"
],
"branch-alias": {
"dev-master": "0.2-dev"
},
"laravel": {
"providers": [
"PragmaRX\\Google2FALaravel\\ServiceProvider"
],
"aliases": {
"Google2FA": "PragmaRX\\Google2FALaravel\\Facade"
}
}
},
"autoload": {
"psr-4": {
"PragmaRX\\Google2FALaravel\\": "src/",
"PragmaRX\\Google2FALaravel\\Tests\\": "tests/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"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": "2020-04-05T17:39:30+00:00"
},
{
"name": "pragmarx/google2fa-qrcode",
"version": "v1.0.3",
"source": {
"type": "git",
"url": "https://github.com/antonioribeiro/google2fa-qrcode.git",
"reference": "fd5ff0531a48b193a659309cc5fb882c14dbd03f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/antonioribeiro/google2fa-qrcode/zipball/fd5ff0531a48b193a659309cc5fb882c14dbd03f",
"reference": "fd5ff0531a48b193a659309cc5fb882c14dbd03f",
"shasum": ""
},
"require": {
"bacon/bacon-qr-code": "~1.0|~2.0",
"php": ">=5.4",
"pragmarx/google2fa": ">=4.0"
},
"require-dev": {
"khanamiryan/qrcode-detector-decoder": "^1.0",
"phpunit/phpunit": "~4|~5|~6|~7"
},
"type": "library",
"extra": {
"component": "package",
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"PragmaRX\\Google2FAQRCode\\": "src/",
"PragmaRX\\Google2FAQRCode\\Tests\\": "tests/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Antonio Carlos Ribeiro",
"email": "acr@antoniocarlosribeiro.com",
"role": "Creator & Designer"
}
],
"description": "QR Code package for Google2FA",
"keywords": [
"2fa",
"Authentication",
"Two Factor Authentication",
"google2fa",
"qr code",
"qrcode"
],
"time": "2019-03-20T16:42:58+00:00"
},
{ {
"name": "psr/container", "name": "psr/container",
"version": "1.0.0", "version": "1.0.0",

79
config/google2fa.php Normal file
View File

@ -0,0 +1,79 @@
<?php
return [
/*
* Enable / disable Google2FA.
*/
'enabled' => env('OTP_ENABLED', true),
/*
* Lifetime in minutes.
*
* In case you need your users to be asked for a new one time passwords from time to time.
*/
'lifetime' => env('OTP_LIFETIME', 0), // 0 = eternal
/*
* Renew lifetime at every new request.
*/
'keep_alive' => env('OTP_KEEP_ALIVE', true),
/*
* Auth container binding.
*/
'auth' => 'auth',
/*
* 2FA verified session var.
*/
'session_var' => 'google2fa',
/*
* One Time Password request input name.
*/
'otp_input' => 'otp',
/*
* One Time Password Window.
*/
'window' => 1,
/*
* Forbid user to reuse One Time Passwords.
*/
'forbid_old_passwords' => false,
/*
* User's table column for google2fa secret.
*/
'otp_secret_column' => 'twofa_secret',
/*
* One Time Password View.
*/
'view' => 'auth.2fa',
/*
* One Time Password error message.
*/
'error_messages' => [
'wrong_otp' => "Your one time code was incorrect.",
'cannot_be_empty' => 'The one time code cannot be empty.',
'unknown' => 'An unknown error has occurred. Please try again.',
],
/*
* Throw exceptions or just fire events?
*/
'throw_exceptions' => env('OTP_THROW_EXCEPTION', true),
/*
* Which image backend to use for generating QR codes?
*
* Supports imagemagick, svg and eps
*/
'qrcode_image_backend' => \PragmaRX\Google2FALaravel\Support\Constants::QRCODE_IMAGE_BACKEND_IMAGEMAGICK,
];

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddTwofaSecretToUsers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('twofa_secret')->nullable()->after('password');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('twofa_secret');
});
}
}

View File

@ -1,6 +0,0 @@
/* overrides customizations for the AdminLTE auth pages */
.login-page, .register-page {
background-image: url('/img/authbg.jpg') !important;
background-size: cover !important;
}

81
public/css/login.css vendored Normal file
View File

@ -0,0 +1,81 @@
body {
font-family: "Karla", sans-serif;
background-color: #d0d0ce;
min-height: 100vh; }
.brand-wrapper {
margin-bottom: 19px; }
.brand-wrapper .logo {
height: 37px; }
.login-card {
border: 0;
border-radius: 27.5px;
box-shadow: 0 10px 30px 0 rgba(172, 168, 168, 0.43);
overflow: hidden; }
.login-card-img {
border-radius: 0;
position: absolute;
width: 100%;
height: 100%;
-o-object-fit: cover;
object-fit: cover; }
.login-card .card-body {
padding: 85px 60px 60px; }
@media (max-width: 422px) {
.login-card .card-body {
padding: 35px 24px; } }
.login-card-description {
font-size: 25px;
color: #000;
font-weight: normal;
margin-bottom: 23px; }
.login-card form {
max-width: 326px; }
.login-card .form-control {
border: 1px solid #d5dae2;
padding: 15px 25px;
margin-bottom: 20px;
min-height: 45px;
font-size: 13px;
line-height: 15;
font-weight: normal; }
.login-card .form-control::-webkit-input-placeholder {
color: #919aa3; }
.login-card .form-control::-moz-placeholder {
color: #919aa3; }
.login-card .form-control:-ms-input-placeholder {
color: #919aa3; }
.login-card .form-control::-ms-input-placeholder {
color: #919aa3; }
.login-card .form-control::placeholder {
color: #919aa3; }
.login-card .login-btn {
padding: 13px 20px 12px;
background-color: #000;
border-radius: 4px;
font-size: 17px;
font-weight: bold;
line-height: 20px;
color: #fff;
margin-bottom: 24px; }
.login-card .login-btn:hover {
border: 1px solid #000;
background-color: transparent;
color: #000; }
.login-card .forgot-password-link {
font-size: 14px;
color: #919aa3;
margin-bottom: 12px; }
.login-card-footer-text {
font-size: 16px;
color: #0d2366;
margin-bottom: 60px; }
@media (max-width: 767px) {
.login-card-footer-text {
margin-bottom: 24px; } }
.login-card-footer-nav a {
font-size: 14px;
color: #919aa3; }
/*# sourceMappingURL=login.css.map */

1
public/css/login.css.map Executable file
View File

@ -0,0 +1 @@
{"version":3,"sources":["login.scss"],"names":[],"mappings":"AAAA;EACI,gCAAgC;EAChC,yBAAyB;EACzB,iBAAiB,EAAA;;AAGrB;EACI,mBAAmB,EAAA;EADvB;IAIQ,YAAY,EAAA;;AAIpB;EACI,SAAS;EACT,qBAAqB;EACrB,mDAAmD;EACnD,gBAAgB,EAAA;EAGhB;IACI,gBAAgB;IAChB,kBAAkB;IAClB,WAAW;IACX,YAAY;IACZ,oBAAiB;OAAjB,iBAAiB,EAAA;EAZzB;IAeQ,uBAAuB,EAAA;IAEvB;MAjBR;QAkBY,kBAAkB,EAAA,EAEzB;EAED;IACI,eAAe;IACf,WAAW;IACX,mBAAmB;IACnB,mBAAmB,EAAA;EA1B3B;IA8BQ,gBAAgB,EAAA;EA9BxB;IAkCQ,yBAAyB;IACzB,kBAAkB;IAClB,mBAAmB;IACnB,gBAAgB;IAChB,eAAe;IACf,eAAe;IACf,mBAAmB,EAAA;IAxC3B;MA2CY,cAAc,EAAA;IA3C1B;MA2CY,cAAc,EAAA;IA3C1B;MA2CY,cAAc,EAAA;IA3C1B;MA2CY,cAAc,EAAA;IA3C1B;MA2CY,cAAc,EAAA;EA3C1B;IAgDQ,uBAAuB;IACvB,sBAAsB;IACtB,kBAAkB;IAClB,eAAe;IACf,iBAAiB;IACjB,iBAAiB;IACjB,WAAW;IACX,mBAAmB,EAAA;IAvD3B;MA0DY,sBAAsB;MACtB,6BAA6B;MAC7B,WAAW,EAAA;EA5DvB;IAiEQ,eAAe;IACf,cAAc;IACd,mBAAmB,EAAA;EAGvB;IACI,eAAe;IACf,cAAc;IACd,mBAAmB,EAAA;IAEnB;MALJ;QAMQ,mBAAmB,EAAA,EAE1B;EAEA;IAEO,eAAe;IACf,cAAc,EAAA","file":"login.css","sourcesContent":["body {\n font-family: \"Karla\", sans-serif;\n background-color: #d0d0ce;\n min-height: 100vh;\n}\n\n.brand-wrapper {\n margin-bottom: 19px;\n\n .logo {\n height: 37px;\n }\n}\n\n.login-card {\n border: 0;\n border-radius: 27.5px;\n box-shadow: 0 10px 30px 0 rgba(172, 168, 168, 0.43);\n overflow: hidden;\n\n\n &-img {\n border-radius: 0;\n position: absolute;\n width: 100%;\n height: 100%;\n object-fit: cover;\n }\n .card-body {\n padding: 85px 60px 60px;\n\n @media (max-width: 422px) {\n padding: 35px 24px;\n }\n }\n\n &-description {\n font-size: 25px;\n color: #000;\n font-weight: normal;\n margin-bottom: 23px;\n }\n\n form {\n max-width: 326px;\n }\n\n .form-control {\n border: 1px solid #d5dae2;\n padding: 15px 25px;\n margin-bottom: 20px;\n min-height: 45px;\n font-size: 13px;\n line-height: 15;\n font-weight: normal;\n\n &::placeholder {\n color: #919aa3;\n }\n }\n\n .login-btn {\n padding: 13px 20px 12px;\n background-color: #000;\n border-radius: 4px;\n font-size: 17px;\n font-weight: bold;\n line-height: 20px;\n color: #fff;\n margin-bottom: 24px;\n\n &:hover {\n border: 1px solid #000;\n background-color: transparent;\n color: #000;\n }\n }\n\n .forgot-password-link {\n font-size: 14px;\n color: #919aa3;\n margin-bottom: 12px;\n }\n\n &-footer-text {\n font-size: 16px;\n color: #0d2366;\n margin-bottom: 60px;\n\n @media (max-width: 767px) {\n margin-bottom: 24px;\n }\n }\n\n &-footer-nav {\n a {\n font-size: 14px;\n color: #919aa3;\n }\n }\n}\n"]}

BIN
public/img/login.jpg Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 KiB

View File

@ -0,0 +1,36 @@
@extends('breadcrumbs.auth.main')
@section('authpage')
<div class="container">
<div class="card login-card">
<div class="row no-gutters">
<div class="col-md-5">
<img src="/img/login.jpg" alt="login" class="login-card-img">
</div>
<div class="col-md-7">
<div class="card-body">
<div class="brand-wrapper">
<img src="{{ config('adminlte.logo_img') }}" alt="logo" class="logo">{{ config('adminlte.logo') }}
</div>
<p class="login-card-description">Two-factor Authentication</p>
<form action="{{ route('verify2FA') }}" method="POST" id="verify">
@csrf
<div class="form-group">
<label for="name" class="sr-only">Two-factor secret code (You can find this on Google Authenticator)</label>
<input type="text" name="otp" id="name" class="form-control" placeholder="2FA Code (e.g. 543324)">
</div>
<input name="register" id="register" class="btn btn-block login-btn mb-4" type="submit" value="Send 2FA Code">
</form>
<p class="login-card-footer-text">Don't know the code? <a href="{{ route('logout') }}" class="text-reset">Cancel login (logout)</a></p>
<nav class="login-card-footer-nav">
<a href="#!">Terms of use</a>
<a href="#!">Privacy policy</a>
</nav>
</div>
</div>
</div>
</div>
</div>
@stop

View File

@ -1 +1,44 @@
@extends('adminlte::auth.login') @extends('breadcrumbs.auth.main')
@section('authpage')
<div class="container">
<div class="card login-card">
<div class="row no-gutters">
<div class="col-md-5">
<img src="/img/login.jpg" alt="login" class="login-card-img">
</div>
<div class="col-md-7">
<div class="card-body">
<div class="brand-wrapper">
<img src="{{ config('adminlte.logo_img') }}" alt="logo" class="logo">{{ config('adminlte.logo') }}
</div>
<p class="login-card-description">Sign into your account</p>
<form action="{{ route('login') }}" method="POST" id="loginForm">
@csrf
<div class="form-group">
<label for="email" class="sr-only">Email</label>
<input type="email" name="email" id="email" class="form-control" placeholder="Email address">
</div>
<div class="form-group mb-4">
<label for="password" class="sr-only">Password</label>
<input type="password" name="password" id="password" class="form-control" placeholder="***********">
</div>
<div class="form-group mb-4">
<label for="remember">Remember me</label>
<input type="checkbox" name="remember" id="remember" />
</div>
<input name="login" id="login" class="btn btn-block login-btn mb-4" type="submit" value="Sign-in">
</form>
<a href="{{ route('password.request') }}" class="forgot-password-link">Forgot password?</a>
<p class="login-card-footer-text">Don't have an account? <a href="{{ route('register') }}" class="text-reset">Register here</a></p>
<nav class="login-card-footer-nav">
<a href="#!">Terms of use</a>
<a href="#!">Privacy policy</a>
</nav>
</div>
</div>
</div>
</div>
</div>
@stop

View File

@ -1 +1,53 @@
@extends('adminlte::auth.register') @extends('breadcrumbs.auth.main')
@section('authpage')
<div class="container">
<div class="card login-card">
<div class="row no-gutters">
<div class="col-md-5">
<img src="/img/login.jpg" alt="login" class="login-card-img">
</div>
<div class="col-md-7">
<div class="card-body">
<div class="brand-wrapper">
<img src="{{ config('adminlte.logo_img') }}" alt="logo" class="logo">{{ config('adminlte.logo') }}
</div>
<p class="login-card-description">Register a new account</p>
<form action="{{ route('register') }}" method="POST" id="registerForm">
@csrf
<div class="form-group">
<label for="name" class="sr-only">Name</label>
<input type="text" name="name" id="name" class="form-control" placeholder="Name (e.g. John Smith)">
</div>
<div class="form-group mb-4">
<label for="email" class="sr-only">Email address</label>
<input type="email" name="email" id="email" class="form-control" placeholder="Email Address">
</div>
<div class="form-group mb-4">
<label for="password" class="sr-only">Password</label>
<input type="password" name="password" id="password" class="form-control" placeholder="Password"
</div>
<div class="form-group mb-4">
<label for="passwordc" class="sr-only">Confirm password</label>
<input type="password" id="passwordc" name="password_confirmation" class="form-control" placeholder="Confirm password" />
</div>
<div class="form-group mt-5">
<label for="mcusername" class="sr-only">Minecraft Username (Premium)</label>
<input type="text" name="uuid" class="form-control" id="mcusername" placeholder="Premium Minecraft Username (e.g. Notch)" />
</div>
<input name="register" id="register" class="btn btn-block login-btn mb-4" type="submit" value="Register">
</form>
<p class="login-card-footer-text">Have an account with us? <a href="{{ route('login') }}" class="text-reset">Login here</a></p>
<nav class="login-card-footer-nav">
<a href="#!">Terms of use</a>
<a href="#!">Privacy policy</a>
</nav>
</div>
</div>
</div>
</div>
</div>
@stop

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>{{ config('app.name') . '| ID' }}</title>
<link href="https://fonts.googleapis.com/css?family=Karla:400,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.materialdesignicons.com/4.8.95/css/materialdesignicons.min.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css" />
<link rel="stylesheet" href="/css/login.css">
</head>
<body>
<main class="d-flex align-items-center min-vh-100 py-3 py-md-0">
@yield('authpage')
</main>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/toastr@2.1.4/toastr.min.js"></script>
<x-global-errors></x-global-errors>
</body>

View File

@ -19,6 +19,92 @@
@stop @stop
@section('content') @section('content')
@if (!Auth::user()->has2FA())
<x-modal id="twoFactorAuthModal" modal-label="2faLabel" modal-title="Two-factor Authentication" include-close-button="true">
<h3><i class="fas fa-user-shield"></i> We're glad you decided to increase your account's security!</h3>
<p><b>Supported apps you can install:</b></p>
<ul>
<li><a href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en"><i class="fab fa-google-play"></i> Google Authenticator</a></li>
</ul>
<p>Scan the <i>QR code</i> below with your preferred app, and then copy the code here.</p>
<div class="row">
<div class="col-3 offset-3">
<div class="qr-code-container text-center">
<img src="{{ $twofaQRCode }}" alt="2FA Security key" />
</div>
</div>
</div>
<div class="row">
<div class="col">
<form method="POST" action="{{ route('enable2FA') }}" id="enable2Fa">
@csrf
@method('PATCH')
<label for="otp">One-time code</label>
<input type="text" id="otp" name="otp" class="form-control" />
</form>
</div>
</div>
<x-slot name="modalFooter">
<button type="button" class="btn btn-success" onclick="$('#enable2Fa').submit()"><i class="fas fa-key"></i> Enable 2FA</button>
</x-slot>
</x-modal>
@endif
@if (Auth::user()->has2FA())
<x-modal id="remove2FA" modal-label="remove2FALabel" modal-title="Remove Two-Factor Authentication" include-close-button="true">
<p><i class="fas fa-exclamation-triangle"></i> <b>Are you sure?</b> Removing two-factor authentication will reduce the security of your account.</p>
<form action="{{ route('disable2FA') }}" method="POST" id="disable2FA">
@csrf
@method('PATCH')
<label for="currentPassword">Confirm your password to continue</label>
<input id="currentPassword" type="password" name="currentPassword" class="form-control" required />
<p class="text-sm text-muted">To prevent unauthorized changes, a password is always required for sensitive operations.</p>
<div class="form-group mt-2">
<label for="consent">"I understand the possible consequences of disabling two factor authentication"</label>
<span><i>Click to confirm </i> </span><input type="checkbox" name="consent" id="consent" required />
</div>
</form>
<x-slot name="modalFooter">
<button type="button" class="btn btn-danger" onclick="$('#disable2FA').submit()"><i class="fa fa-trash"></i> Remove 2FA</button>
</x-slot>
</x-modal>
@endif
<div class="modal fade" tabindex="-1" id="authenticationForm" role="dialog" aria-labelledby="authenticationFormLabel" aria-hidden="true"> <div class="modal fade" tabindex="-1" id="authenticationForm" role="dialog" aria-labelledby="authenticationFormLabel" aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
@ -116,8 +202,16 @@
</div> </div>
<div class="tab-pane fade p-3" id="twofa" role="tabpanel" aria-labelledby="twofaTab"> <div class="tab-pane fade p-3" id="twofa" role="tabpanel" aria-labelledby="twofaTab">
<h5 class="card-title">Two-factor Authentication</h5> <h5 class="card-title">Two-factor Authentication</h5>
<p class="card-text"><b>This feature is not yet available.</b> Support for Google Authenticator, Authy, Microsoft Authenticator and other compatible apps is coming soon, as well as fingerprint login for android devices.</p> <br />
<button type="button" class="btn btn-primary" disabled>Enable 2FA</button> @if (Auth::user()->has2FA())
<p><b>Hooray!</b> 2FA is setup correctly for your account. A code will be asked each time you login.</p>
<button type="button" class="btn btn-danger" onclick="$('#remove2FA').modal('show')"><i class="fa fa-ban"></i> Disable 2FA (not recommended)</button>
@else
<p class="card-text"><b>Two-factor auth is available for your account.</b> Enabling this security option greatly increases your account's security in case your password ever gets stolen.</p>
<button type="button" class="btn btn-primary" onclick="$('#twoFactorAuthModal').modal('show')">Enable 2FA</button>
@endif
</div> </div>
<div class="tab-pane fade p-3" id="sessions" role="tabpanel" aria-labelledby="sessionsTab"> <div class="tab-pane fade p-3" id="sessions" role="tabpanel" aria-labelledby="sessionsTab">
<h5 class="card-title">Session Manager</h5> <h5 class="card-title">Session Manager</h5>

View File

@ -16,6 +16,9 @@ Route::group(['prefix' => 'auth', 'middleware' => ['usernameUUID']], function ()
Auth::routes(); Auth::routes();
Route::post('/twofa/authenticate', 'Auth\TwofaController@verify2FA')
->name('verify2FA');
}); });
Route::get('/','HomeController@index') Route::get('/','HomeController@index')
@ -25,7 +28,7 @@ Route::post('/form/contact', 'ContactController@create')
->name('sendSubmission'); ->name('sendSubmission');
Route::group(['middleware' => ['auth', 'forcelogout']], function(){ Route::group(['middleware' => ['auth', 'forcelogout', '2fa']], function(){
Route::get('/dashboard', 'DashboardController@index') Route::get('/dashboard', 'DashboardController@index')
->name('dashboard') ->name('dashboard')
@ -129,6 +132,12 @@ Route::group(['middleware' => ['auth', 'forcelogout']], function(){
Route::post('/settings/account/flush-sessions', 'UserController@flushSessions') Route::post('/settings/account/flush-sessions', 'UserController@flushSessions')
->name('flushSessions'); ->name('flushSessions');
Route::patch('/settings/account/twofa/enable', 'UserController@add2FASecret')
->name('enable2FA');
Route::patch('/settings/account/twofa/disable', 'UserController@remove2FASecret')
->name('disable2FA');
}); });