user(); $data = $this->mfaService->setupTotp($user); return $this->success($data, 'TOTP setup started'); } public function confirmTotp(MfaConfirmRequest $request): JsonResponse { $user = $request->user(); try { $backupCodes = $this->mfaService->confirmTotp($user, $request->validated('code')); } catch (\DomainException $e) { return $this->error($e->getMessage(), 422); } return $this->success([ 'mfa_enabled' => true, 'method' => MfaMethod::TOTP->value, 'backup_codes' => $backupCodes, ], 'TOTP enabled'); } public function setupEmail(Request $request): JsonResponse { $user = $request->user(); try { $this->mfaService->setupEmail($user); } catch (\DomainException $e) { return $this->error($e->getMessage(), 429); } return $this->success(null, 'Verification code sent'); } public function confirmEmail(MfaConfirmRequest $request): JsonResponse { $user = $request->user(); try { $backupCodes = $this->mfaService->confirmEmail($user, $request->validated('code')); } catch (\DomainException $e) { return $this->error($e->getMessage(), 422); } return $this->success([ 'mfa_enabled' => true, 'method' => MfaMethod::EMAIL->value, 'backup_codes' => $backupCodes, ], 'Email MFA enabled'); } public function disable(MfaDisableRequest $request): JsonResponse { $user = $request->user(); if (! $user->mfa_enabled) { return $this->error('MFA is niet ingeschakeld.', 422); } // Verify the code before disabling $method = MfaMethod::from($request->validated('method')); $code = $request->validated('code'); try { if ($method === MfaMethod::TOTP) { $secret = decrypt($user->mfa_secret); $google2fa = app(\PragmaRX\Google2FA\Google2FA::class); if (! $google2fa->verifyKey($secret, $code, 1)) { throw new \DomainException('Ongeldige verificatiecode.'); } } elseif ($method === MfaMethod::BACKUP_CODE) { // Backup code verification is handled by verifying against stored hashes // We need to check manually here since verifyBackupCode is private $this->verifyBackupCodeForDisable($user, $code); } } catch (\DomainException $e) { return $this->error($e->getMessage(), 422); } $this->mfaService->disable($user); return $this->success(null, 'MFA disabled'); } public function regenerateBackupCodes(MfaConfirmRequest $request): JsonResponse { $user = $request->user(); // Verify TOTP code before regenerating try { if ($user->mfa_method === MfaMethod::TOTP->value) { $secret = decrypt($user->mfa_secret); $google2fa = app(\PragmaRX\Google2FA\Google2FA::class); if (! $google2fa->verifyKey($secret, $request->validated('code'), 1)) { throw new \DomainException('Ongeldige verificatiecode.'); } } $codes = $this->mfaService->regenerateBackupCodes($user); } catch (\DomainException $e) { return $this->error($e->getMessage(), 422); } return $this->success([ 'backup_codes' => $codes, ], 'Backup codes regenerated'); } public function status(Request $request): JsonResponse { $user = $request->user(); return $this->success([ 'mfa_enabled' => $user->mfa_enabled, 'method' => $user->mfa_method, 'confirmed_at' => $user->mfa_confirmed_at?->toIso8601String(), 'backup_codes_remaining' => $this->mfaService->getRemainingBackupCodeCount($user), 'is_required' => $this->mfaService->isMfaRequired($user), ]); } private function verifyBackupCodeForDisable(\App\Models\User $user, string $code): void { $normalizedCode = strtoupper(str_replace([' ', '-'], '', $code)); $backupCodes = \App\Models\MfaBackupCode::where('user_id', $user->id) ->where('used', false) ->get(); foreach ($backupCodes as $backupCode) { if (\Illuminate\Support\Facades\Hash::check($code, $backupCode->code_hash) || \Illuminate\Support\Facades\Hash::check($normalizedCode, $backupCode->code_hash)) { $backupCode->update([ 'used' => true, 'used_at' => now(), ]); return; } } throw new \DomainException('Ongeldige backup code.'); } }