-
-
Notifications
You must be signed in to change notification settings - Fork 178
feat: 2fa auth #1343
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MrWeez
wants to merge
48
commits into
Ctrlpanel-gg:development
Choose a base branch
from
MrWeez:feat/2fa
base: development
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
feat: 2fa auth #1343
Changes from all commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
f14b9a8
fix: editorconfig for blade files (4 spaces instead of 2)
MrWeez a6c61e2
refactor: tabbed profile page
MrWeez 8552e69
refactor: improve structure of profile page
MrWeez dbf055c
feat: add two-factor database migrations and models
MrWeez daf739c
feat: add two-factor service layer with TOTP and recovery code support
MrWeez 42ed623
feat: add two-factor middleware with impersonation bypass and registe…
MrWeez 6f2458e
feat: integrate two-factor challenge into login and logout flow
MrWeez 0b122b5
feat: add TOTP challenge controller and view
MrWeez 9d4a890
feat: add TOTP activation flow with QR setup and recovery code manage…
MrWeez 6a56928
feat: add TOTP management UI to profile security tab
MrWeez a5c6399
feat: add rate limiting, token pruning, and security hardening
MrWeez 00cebba
feat: implement modular 2FA core infrastructure
MrWeez a842db1
feat: enhance ExtensionServiceProvider to auto-load 2FA assets
MrWeez 069bd51
refactor: move TOTP logic into modular extension
MrWeez c8326bb
feat: implement dynamic 2FA picker and update UI/routes
MrWeez d99aa4e
test: add Dummy 2FA extension for multi-method testing
MrWeez 2085e18
style: standardize app name default to CtrlPanel.gg across the project
MrWeez 632b3ef
fix: 2fa action route name and throttle
MrWeez 5cde507
feat: improve views for 2FA and profile
MrWeez 6c85aba
feat: add theming guide for 2FA system
MrWeez d25fadb
fix: dummy method profile card and login layout
MrWeez 2ab9241
security: fix action whitelist and dummy method availability
MrWeez d768290
security: fix token expiration logic and path traversal in extensions
MrWeez 4ff5650
refactor: add route constraints and tighten model guarding
MrWeez bb5a441
perf: fix N+1 in profile and cleanup redundant tokens
MrWeez 7547a81
docs: clarify google2fa configuration
MrWeez c30773d
fix: extend 2fa token expiration time to match remember cookie
MrWeez 5afd8d2
fix: correct 2FA token lifetime config and persistence logic
MrWeez 85463cd
style: optimize 2FA UI and refactor profile data injection
MrWeez 7cd9606
docs: add warnings and optimize recovery code verification logic
MrWeez 28b060b
refactor: 2fa modals and profile views
MrWeez ca3d2ce
Merge branch 'development' into feat/2fa
MrWeez 5f6e524
Merge branch 'development' into feat/2fa
MrWeez 3afc51d
refactor: use SweetAlert2 toasts instead of toastr for 2FA
MrWeez 8a173ba
refactor: use proper dependency injection for 2FA services across con…
MrWeez 80a536d
style: remove useless coment
MrWeez ab407a5
fix: lower throttle limits for some 2fa routes
MrWeez 997bdd9
feat: add 2FA status badge to admin datatable and raw method names to…
MrWeez 19e24e4
feat: add cp:user:2fa:disable artisan command for 2FA recovery
MrWeez 606043c
fix: make TwoFactorService CLI-safe by checking for active session
MrWeez 784b4e2
fix: display none if no 2fa methods activated
MrWeez 1738a27
refactor: disable 2fa command
MrWeez d4c15ea
Merge branch 'development' into feat/2fa
MrWeez b78e592
refactor: remove unused TwoFactorService dependency from UserController
MrWeez 7e1b5a9
security: restrict Dummy 2FA extension to local environment only
MrWeez 5b215a9
feat: implement dynamic, extension-aware 2FA rate limiting
MrWeez 6b166d6
docs: complete 2FA modular system documentation with custom routes an…
MrWeez 22ef0dc
fix: restore accidentally removed web rate limiter
MrWeez File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| <?php | ||
|
|
||
| namespace App\Classes; | ||
|
|
||
| use App\Models\User; | ||
| use Illuminate\Http\Request; | ||
|
|
||
| abstract class TwoFactorExtension extends AbstractExtension | ||
| { | ||
| /** | ||
| * Get the unique identifier for this 2FA method. | ||
| */ | ||
| abstract public function getName(): string; | ||
|
|
||
| /** | ||
| * Get the display label for this 2FA method. | ||
| */ | ||
| abstract public function getLabel(): string; | ||
|
|
||
| /** | ||
| * Get the FontAwesome icon for this 2FA method. | ||
| */ | ||
| abstract public function getIcon(): string; | ||
|
|
||
| /** | ||
| * Get a short description for this 2FA method. | ||
| */ | ||
| abstract public function getDescription(): string; | ||
|
|
||
| /** | ||
| * Check if this 2FA method is available for the given user. | ||
| */ | ||
| public function isAvailable(User $user): bool | ||
| { | ||
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Get the view name for the settings card in the profile. | ||
| */ | ||
| abstract public function getSettingsView(): string; | ||
|
|
||
| /** | ||
| * Get the view name for the login challenge. | ||
| */ | ||
| abstract public function getChallengeView(): string; | ||
|
|
||
| /** | ||
| * Verify the 2FA challenge. | ||
| */ | ||
| abstract public function verify(Request $request): bool; | ||
|
|
||
| /** | ||
| * Handle the setup logic (AJAX). | ||
| */ | ||
| abstract public function setup(Request $request); | ||
|
|
||
| /** | ||
| * Handle the enable logic (AJAX). | ||
| */ | ||
| abstract public function enable(Request $request); | ||
|
|
||
| /** | ||
| * Handle the disable logic (AJAX). | ||
| */ | ||
| abstract public function disable(Request $request); | ||
|
|
||
| /** | ||
| * Get the list of allowed actions that can be called via the action route. | ||
| * | ||
| * @return array | ||
| */ | ||
| public function getAllowedActions(): array | ||
| { | ||
| return []; | ||
| } | ||
|
|
||
| /** | ||
| * Get the rate limit for a specific action. | ||
| * | ||
| * @param string $action (setup, enable, disable, verify, action) | ||
| * @return array ['attempts' => int, 'minutes' => int] | ||
| */ | ||
| public function getRateLimit(string $action): array | ||
| { | ||
| $defaults = [ | ||
| 'setup' => ['attempts' => 3, 'minutes' => 1], | ||
| 'enable' => ['attempts' => 3, 'minutes' => 1], | ||
| 'disable' => ['attempts' => 3, 'minutes' => 1], | ||
| 'verify' => ['attempts' => 5, 'minutes' => 1], | ||
| 'action' => ['attempts' => 3, 'minutes' => 5], // Default 3 per 5 mins for sensitive actions | ||
| ]; | ||
|
|
||
| return $this->getConfig()['rate_limits'][$action] ?? $defaults[$action]; | ||
| } | ||
|
|
||
| /** | ||
| * Get method-specific routes configuration if any. | ||
| */ | ||
| public static function getConfig(): array | ||
| { | ||
| return []; | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| <?php | ||
|
|
||
| namespace App\Console\Commands; | ||
|
|
||
| use App\Models\User; | ||
| use App\Services\TwoFactor\TwoFactorService; | ||
| use Illuminate\Console\Command; | ||
|
|
||
| class DisableTwoFactorCommand extends Command | ||
| { | ||
| /** | ||
| * The name and signature of the console command. | ||
| * | ||
| * @var string | ||
| */ | ||
| protected $signature = 'cp:user:2fa:disable {search? : The ID, Email, Username or Discord ID of the user}'; | ||
|
|
||
| /** | ||
| * The console command description. | ||
| * | ||
| * @var string | ||
| */ | ||
| protected $description = 'Forcibly disable 2FA methods for a user'; | ||
|
|
||
| protected $twoFactorService; | ||
|
|
||
| public function __construct(TwoFactorService $twoFactorService) | ||
| { | ||
| parent::__construct(); | ||
| $this->twoFactorService = $twoFactorService; | ||
| } | ||
|
|
||
| /** | ||
| * Execute the console command. | ||
| */ | ||
| public function handle() | ||
| { | ||
| $search = $this->argument('search'); | ||
|
|
||
| if (!$search) { | ||
| $search = $this->ask('Please enter User ID, Email, Username or Discord ID'); | ||
| } | ||
|
|
||
| if (!$search) { | ||
| $this->error('No search term provided.'); | ||
| return 1; | ||
| } | ||
|
|
||
| $user = User::query() | ||
| ->where('id', $search) | ||
| ->orWhere('email', $search) | ||
| ->orWhere('name', $search) | ||
| ->orWhereHas('discordUser', function ($query) use ($search) { | ||
| $query->where('id', $search); | ||
| }) | ||
| ->first(); | ||
|
|
||
| if (!$user) { | ||
| $this->error("User not found with term: {$search}"); | ||
| return 1; | ||
| } | ||
|
|
||
| $this->info("Found User: {$user->name} ({$user->email}) [ID: {$user->id}]"); | ||
|
|
||
| $methods = $user->twoFactorMethods()->where('is_enabled', true)->get(); | ||
|
|
||
| if ($methods->isEmpty()) { | ||
| $this->warn('This user does not have any 2FA methods enabled.'); | ||
| return 0; | ||
| } | ||
|
|
||
| $choices = $methods->mapWithKeys(function ($m) { | ||
| $label = $this->twoFactorService->getExtension($m->method)?->getLabel() ?? ucfirst($m->method); | ||
| return [$m->method => "{$label} ({$m->method})"]; | ||
| })->toArray(); | ||
|
|
||
| $choices['all'] = 'Disable ALL methods'; | ||
|
|
||
| $selected = $this->choice( | ||
| 'Which 2FA methods do you want to disable?', | ||
| $choices, | ||
| null, | ||
| null, | ||
| true // Multiple selection | ||
| ); | ||
|
|
||
| if (empty($selected)) { | ||
| $this->info('Nothing selected. Aborting.'); | ||
| return 0; | ||
| } | ||
|
|
||
| if (in_array('Disable ALL methods', $selected) || in_array('all', $selected)) { | ||
| if ($this->confirm("Are you sure you want to disable ALL 2FA methods for {$user->name}?", true)) { | ||
| $user->twoFactorMethods()->delete(); | ||
| $this->twoFactorService->clearVerified(request(), $user); | ||
| $this->success("Successfully disabled all 2FA methods for {$user->name}."); | ||
| } | ||
| return 0; | ||
| } | ||
|
|
||
| // Map back titles to keys if necessary (Laravel's choice with multiple can return values or keys depending on version/selection) | ||
| $methodKeys = []; | ||
| foreach ($selected as $choice) { | ||
| $key = array_search($choice, $choices); | ||
| if ($key !== false) { | ||
| $methodKeys[] = $key; | ||
| } else { | ||
| // If it already returned the key | ||
| if (isset($choices[$choice])) { | ||
| $methodKeys[] = $choice; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if ($this->confirm("Disable selected methods: " . implode(', ', $methodKeys) . "?", true)) { | ||
| $user->twoFactorMethods()->whereIn('method', $methodKeys)->delete(); | ||
|
|
||
| // If we disabled everything, clear verified state | ||
| if ($user->twoFactorMethods()->where('is_enabled', true)->count() === 0) { | ||
| $this->twoFactorService->clearVerified(request(), $user); | ||
| } | ||
|
|
||
| $this->success("Successfully disabled " . implode(', ', $methodKeys) . " for {$user->name}."); | ||
| } | ||
|
|
||
| return 0; | ||
| } | ||
|
|
||
| protected function success($message) | ||
| { | ||
| $this->output->writeln("<fg=green;options=bold> SUCCESS </> $message"); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| <?php | ||
|
|
||
| namespace App\Extensions\TwoFactor\Dummy; | ||
|
|
||
| use App\Classes\TwoFactorExtension; | ||
| use App\Models\User; | ||
| use App\Models\UserTwoFactorMethod; | ||
| use Illuminate\Http\Request; | ||
|
|
||
| class DummyExtension extends TwoFactorExtension | ||
| { | ||
| public function getName(): string | ||
| { | ||
| return 'dummy'; | ||
| } | ||
|
|
||
| public function getLabel(): string | ||
| { | ||
| return __('Dummy 2FA (Test)'); | ||
| } | ||
|
|
||
| public function getIcon(): string | ||
| { | ||
| return 'fas fa-flask'; | ||
| } | ||
|
|
||
| public function getDescription(): string | ||
| { | ||
| return __('A temporary method for testing modular 2FA.'); | ||
| } | ||
|
|
||
| public function isAvailable(User $user): bool | ||
| { | ||
| return app()->environment('local'); | ||
| } | ||
|
|
||
| public function getSettingsView(): string | ||
| { | ||
| if (!app()->environment('local')) { | ||
| abort(403); | ||
| } | ||
|
|
||
| return 'twofactor_dummy::profile_card'; | ||
| } | ||
|
|
||
| public function getChallengeView(): string | ||
| { | ||
| if (!app()->environment('local')) { | ||
| abort(403); | ||
| } | ||
|
|
||
| return 'twofactor_dummy::auth.two-factor.dummy-challenge'; | ||
| } | ||
|
|
||
| public function verify(Request $request): bool | ||
| { | ||
| if (!app()->environment('local')) { | ||
| abort(403); | ||
| } | ||
|
|
||
| return $request->input('code') === '123456'; | ||
| } | ||
|
|
||
| public function setup(Request $request) | ||
| { | ||
| if (!app()->environment('local')) { | ||
| abort(403); | ||
| } | ||
|
|
||
| return response()->json(['message' => 'Dummy setup ready. Use code 123456 to enable.']); | ||
| } | ||
|
|
||
| public function enable(Request $request) | ||
| { | ||
| if (!app()->environment('local')) { | ||
| abort(403); | ||
| } | ||
|
|
||
| if ($request->input('code') !== '123456') { | ||
| return response()->json(['errors' => ['code' => ['Use 123456']]], 422); | ||
| } | ||
|
|
||
| UserTwoFactorMethod::updateOrCreate( | ||
| ['user_id' => $request->user()->id, 'method' => 'dummy'], | ||
| ['is_enabled' => true] | ||
| ); | ||
|
|
||
| return response()->json(['message' => 'Dummy 2FA enabled!']); | ||
| } | ||
|
|
||
| /** | ||
| * NOTE: This is a dummy method for development only. | ||
| * In a production-ready extension, this method SHOULD require | ||
| * password or 2FA code verification before disabling. | ||
| */ | ||
| public function disable(Request $request) | ||
| { | ||
| if (!app()->environment('local')) { | ||
| abort(403); | ||
| } | ||
|
|
||
| $request->user()->twoFactorMethods()->where('method', 'dummy')->delete(); | ||
| return response()->json(['message' => 'Dummy 2FA disabled.']); | ||
| } | ||
| } | ||
27 changes: 27 additions & 0 deletions
27
app/Extensions/TwoFactor/Dummy/views/auth/two-factor/dummy-challenge.blade.php
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| @extends('layouts.app') | ||
|
|
||
| @section('content') | ||
| @php($suppressSweetAlert2 = true) | ||
|
|
||
| <body class="hold-transition dark-mode login-page"> | ||
| <div class="login-box"> | ||
| <div class="card card-outline card-primary"> | ||
| <div class="text-center card-header"> | ||
| <a href="{{ route('welcome') }}" class="mb-2 h1"><b class="mr-1">{{ config('app.name', 'CtrlPanel.gg') }}</b></a> | ||
| </div> | ||
| <div class="card-body"> | ||
| <p class="login-box-msg">{{ __('Dummy 2FA Challenge') }}</p> | ||
| <p class="text-center small text-muted">Enter 123456 to pass.</p> | ||
|
|
||
| <form action="{{ route('login.2fa.verify', ['method' => 'dummy']) }}" method="post"> | ||
| @csrf | ||
| <div class="form-group"> | ||
| <input type="text" name="code" class="form-control" placeholder="123456" autofocus> | ||
| </div> | ||
| <button type="submit" class="btn btn-primary btn-block">{{ __('Verify') }}</button> | ||
| </form> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| </body> | ||
| @endsection |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.