Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

# Access
ADMIN_REQUIRES_2FA=true

CHANGE_EMAIL=true
ENABLE_REGISTRATION=true
PASSWORD_HISTORY=3
Expand All @@ -78,6 +79,10 @@ REGISTRATION_CAPTCHA_STATUS=false
INVISIBLE_RECAPTCHA_SITEKEY=
INVISIBLE_RECAPTCHA_SECRETKEY=

# Recovery code settings
RECOVERY_CODES_COUNT=8
RECOVERY_CODE_LENGTH=10

# Socialite Providers
FACEBOOK_ACTIVE=false
BITBUCKET_ACTIVE=false
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,4 @@ If you discover a security vulnerability within this boilerplate, please send an
### License

MIT: [http://anthony.mit-license.org](http://anthony.mit-license.org)
$2y$10$McIBAe317YCY6U/2udeGLu3uUAlyRL/.g903vW19R34r1wNscTeAW
23 changes: 23 additions & 0 deletions app/Console/Commands/CleanupRecoveryCodes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace App\Console\Commands;

use App\Domains\Auth\Models\RecoveryCode;
use Illuminate\Console\Command;

class CleanupRecoveryCodes extends Command
{
protected $signature = 'auth:cleanup-recovery-codes {--days=90}';
protected $description = 'Clean up old used recovery codes';

public function handle()
{
$days = $this->option('days');

$deleted = RecoveryCode::used()
->where('used_at', '<', now()->subDays($days))
->delete();

$this->info("Cleaned up {$deleted} old recovery codes.");
}
}
14 changes: 11 additions & 3 deletions app/Domains/Announcement/Models/Announcement.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions;

/**
* Class Announcement.
Expand All @@ -20,9 +21,6 @@ class Announcement extends Model
public const TYPE_FRONTEND = 'frontend';
public const TYPE_BACKEND = 'backend';

protected static $logFillable = true;
protected static $logOnlyDirty = true;

/**
* @var string[]
*/
Expand Down Expand Up @@ -50,6 +48,16 @@ class Announcement extends Model
'enabled' => 'boolean',
];

/**
* Required method for spatie/laravel-activitylog v4.x
*/
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logFillable()
->logOnlyDirty();
}

/**
* Create a new factory instance for the model.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Domains\Auth\Http\Controllers\Frontend\Auth;

use App\Domains\Auth\Http\Requests\Frontend\Auth\DisableTwoFactorAuthenticationRequest;
use PragmaRX\Google2FALaravel\Support\Authenticator;

/**
* Class DisableTwoFactorAuthenticationController.
Expand All @@ -23,8 +24,18 @@ public function show()
*/
public function destroy(DisableTwoFactorAuthenticationRequest $request)
{
$request->user()->disableTwoFactorAuth();
$user = $request->user();
$google2fa = app('pragmarx.google2fa');

return redirect()->route('frontend.user.account', ['#two-factor-authentication'])->withFlashSuccess(__('Two Factor Authentication Successfully Disabled'));
$valid = $google2fa->verifyKey($user->google2fa_secret, $request->code);

if (!$valid) {
return back()->withErrors(['code' => 'Invalid 2FA code']);
}

$user->disableTwoFactorAuth();

return redirect()->route('frontend.user.account', ['#two-factor-authentication'])
->withFlashSuccess(__('Two Factor Authentication Successfully Disabled'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Request;
use LangleyFoxall\LaravelNISTPasswordRules\PasswordRules;
use Illuminate\Validation\Rules\Password;

/**
* Class LoginController.
Expand Down Expand Up @@ -59,7 +59,10 @@ protected function validateLogin(Request $request)
{
$request->validate([
$this->username() => ['required', 'max:255', 'string'],
'password' => array_merge(['max:100'], PasswordRules::login()),
'password' => [
'required',
'max:100',
],
'g-recaptcha-response' => ['required_if:captcha_status,true', new Captcha],
], [
'g-recaptcha-response.required_if' => __('validation.required', ['attribute' => 'captcha']),
Expand All @@ -68,7 +71,7 @@ protected function validateLogin(Request $request)

/**
* Overidden for 2FA
* https://github.com/DarkGhostHunter/Laraguard#protecting-the-login.
* https://github.com/antonioribeiro/google2fa-laravel.
*
* Attempt to log the user into the application.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use LangleyFoxall\LaravelNISTPasswordRules\PasswordRules;
use Illuminate\Validation\Rules\Password;

/**
* Class RegisterController.
Expand Down Expand Up @@ -75,7 +75,17 @@ protected function validator(array $data)
return Validator::make($data, [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users')],
'password' => array_merge(['max:100'], PasswordRules::register($data['email'] ?? null)),
'password' => [
'required',
'confirmed',
'max:100',
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised()
],
'terms' => ['required', 'in:1'],
'g-recaptcha-response' => ['required_if:captcha_status,true', new Captcha],
], [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
use LangleyFoxall\LaravelNISTPasswordRules\PasswordRules;
use Illuminate\Validation\Rules\Password;

/**
* Class ResetPasswordController.
Expand Down Expand Up @@ -51,13 +51,18 @@ protected function rules()
return [
'token' => ['required'],
'email' => ['required', 'max:255', 'email'],
'password' => array_merge(
[
'max:100',
new UnusedPassword(request('email')),
],
PasswordRules::changePassword(request('email'))
),
'password' => [
'required',
'confirmed', // Important for password reset - ensures password_confirmation field matches
'max:100',
new UnusedPassword(request('email')),
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised()
],
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Domains\Auth\Http\Controllers\Frontend\Auth;

use Illuminate\Http\Request;
use PragmaRX\Google2FALaravel\Support\Authenticator;

/**
* Class TwoFactorAuthenticationController.
Expand All @@ -15,11 +16,25 @@ class TwoFactorAuthenticationController
*/
public function create(Request $request)
{
$secret = $request->user()->createTwoFactorAuth();
$user = $request->user();
$google2fa = app('pragmarx.google2fa');

$secret = $google2fa->generateSecretKey();

// Store secret temporarily in session until confirmed
session(['google2fa_secret' => $secret]);

$qrCodeUrl = $google2fa->getQRCodeUrl(
config('app.name'),
$user->email,
$secret
);

$qrCode = \SimpleSoftwareIO\QrCode\Facades\QrCode::size(200)->generate($qrCodeUrl);

return view('frontend.user.account.tabs.two-factor-authentication.enable')
->withQrCode($secret->toQr())
->withSecret($secret->toString());
->withQrCode($qrCode)
->withSecret($secret);
}

/**
Expand All @@ -28,8 +43,17 @@ public function create(Request $request)
*/
public function show(Request $request)
{
return view('frontend.user.account.tabs.two-factor-authentication.recovery')
->withRecoveryCodes($request->user()->getRecoveryCodes());
$user = $request->user();

// Get recovery codes metadata (without plain codes)
$recoveryCodes = $user->getRecoveryCodes();
$unusedCount = $user->getUnusedRecoveryCodesCount();

return view('frontend.user.account.tabs.two-factor-authentication.recovery', [
'recoveryCodes' => $recoveryCodes,
'unusedCount' => $unusedCount,
'hasUnusedCodes' => $user->hasUnusedRecoveryCodes(),
]);
}

/**
Expand All @@ -38,10 +62,15 @@ public function show(Request $request)
*/
public function update(Request $request)
{
$request->user()->generateRecoveryCodes();
$user = $request->user();

// Generate new recovery codes
$newCodes = $user->generateRecoveryCodes();

session()->flash('flash_warning', __('Any old backup codes have been invalidated.'));
session()->flash('new_recovery_codes', $newCodes);

return redirect()->route('frontend.auth.account.2fa.show')->withFlashSuccess(__('Two Factor Recovery Codes Regenerated'));
return redirect()->route('frontend.auth.account.2fa.show')
->withFlashSuccess(__('Two Factor Recovery Codes Regenerated'));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@ public function handle($request, Closure $next, $status = 'enabled')
return $next($request);
}

// Check if user has Google2FA enabled (has google2fa_secret)
$userHas2FA = !empty($request->user()->google2fa_secret);

// Page requires 2fa, but user is not enabled or page does not require 2fa, but it is enabled
if (
($status === 'enabled' && ! $request->user()->hasTwoFactorEnabled()) ||
($status === 'disabled' && $request->user()->hasTwoFactorEnabled())
($status === 'enabled' && ! $userHas2FA) ||
($status === 'disabled' && $userHas2FA)
) {
return redirect()->route('frontend.auth.account.2fa.create')->withFlashDanger(__('Two-factor Authentication must be :status to view this page.', ['status' => $status]));
return redirect()->route('frontend.auth.account.2fa.create')
->withFlashDanger(__('Two-factor Authentication must be :status to view this page.', ['status' => $status]));
}

return $next($request);
Expand Down
14 changes: 12 additions & 2 deletions app/Domains/Auth/Http/Requests/Backend/User/StoreUserRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use App\Domains\Auth\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use LangleyFoxall\LaravelNISTPasswordRules\PasswordRules;
use Illuminate\Validation\Rules\Password;

/**
* Class StoreUserRequest.
Expand Down Expand Up @@ -33,7 +33,17 @@ public function rules()
'type' => ['required', Rule::in([User::TYPE_ADMIN, User::TYPE_USER])],
'name' => ['required', 'max:100'],
'email' => ['required', 'max:255', 'email', Rule::unique('users')],
'password' => ['max:100', PasswordRules::register($this->email)],
'password' => [
'required',
'confirmed',
'max:100',
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised()
],
'active' => ['sometimes', 'in:1'],
'email_verified' => ['sometimes', 'in:1'],
'send_confirmation_email' => ['sometimes', 'in:1'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use App\Domains\Auth\Rules\UnusedPassword;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Foundation\Http\FormRequest;
use LangleyFoxall\LaravelNISTPasswordRules\PasswordRules;
use Illuminate\Validation\Rules\Password;

/**
* Class UpdateUserPasswordRequest.
Expand All @@ -30,13 +30,20 @@ public function authorize()
public function rules()
{
return [
'password' => array_merge(
[
'max:100',
new UnusedPassword((int) $this->segment(4)),
],
PasswordRules::changePassword($this->email)
),
'token' => ['required'],
'email' => ['required', 'max:255', 'email'],
'password' => [
'required',
'confirmed', // Important for password reset - ensures password_confirmation field matches
'max:100',
new UnusedPassword(request('email')),
Password::min(8)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised()
],
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class DisableTwoFactorAuthenticationRequest extends FormRequest
*/
public function authorize()
{
return true;
return $this->user()->hasTwoFactorEnabled();
}

/**
Expand All @@ -27,7 +27,7 @@ public function authorize()
public function rules()
{
return [
'code' => ['required', 'max:10', 'totp_code'],
'code' => 'required|digits:6',
];
}
}
Loading