fix: skip password confirmation for OAuth users

OAuth users don't have passwords set, so they should not be prompted for password confirmation when performing destructive actions. This fix:
- Detects OAuth users via the hasPassword() method
- Skips password confirmation in modal for OAuth users
- Keeps text name confirmation as the final step
- Centralizes logic in helper functions for maintainability
- Changes button text to "Confirm" when password step is skipped

Fixes #4457

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2025-12-12 14:12:02 +01:00
parent 366ff95893
commit b0d50669b1
16 changed files with 109 additions and 120 deletions

View File

@@ -2,10 +2,8 @@
namespace App\Livewire; namespace App\Livewire;
use App\Models\InstanceSettings;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class NavbarDeleteTeam extends Component class NavbarDeleteTeam extends Component
@@ -19,12 +17,8 @@ class NavbarDeleteTeam extends Component
public function delete($password) public function delete($password)
{ {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! verifyPasswordConfirmation($password, $this)) {
if (! Hash::check($password, Auth::user()->password)) { return;
$this->addError('password', 'The provided password is incorrect.');
return;
}
} }
$currentTeam = currentTeam(); $currentTeam = currentTeam();

View File

@@ -2,12 +2,9 @@
namespace App\Livewire\Project\Database; namespace App\Livewire\Project\Database;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use Exception; use Exception;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Locked; use Livewire\Attributes\Locked;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
@@ -154,12 +151,8 @@ class BackupEdit extends Component
{ {
$this->authorize('manageBackups', $this->backup->database); $this->authorize('manageBackups', $this->backup->database);
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! verifyPasswordConfirmation($password, $this)) {
if (! Hash::check($password, Auth::user()->password)) { return;
$this->addError('password', 'The provided password is incorrect.');
return;
}
} }
try { try {

View File

@@ -2,11 +2,9 @@
namespace App\Livewire\Project\Database; namespace App\Livewire\Project\Database;
use App\Models\InstanceSettings;
use App\Models\ScheduledDatabaseBackup; use App\Models\ScheduledDatabaseBackup;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class BackupExecutions extends Component class BackupExecutions extends Component
@@ -69,12 +67,8 @@ class BackupExecutions extends Component
public function deleteBackup($executionId, $password) public function deleteBackup($executionId, $password)
{ {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! verifyPasswordConfirmation($password, $this)) {
if (! Hash::check($password, Auth::user()->password)) { return;
$this->addError('password', 'The provided password is incorrect.');
return;
}
} }
$execution = $this->backup->executions()->where('id', $executionId)->first(); $execution = $this->backup->executions()->where('id', $executionId)->first();

View File

@@ -4,12 +4,9 @@ namespace App\Livewire\Project\Service;
use App\Actions\Database\StartDatabaseProxy; use App\Actions\Database\StartDatabaseProxy;
use App\Actions\Database\StopDatabaseProxy; use App\Actions\Database\StopDatabaseProxy;
use App\Models\InstanceSettings;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class Database extends Component class Database extends Component
@@ -96,12 +93,8 @@ class Database extends Component
try { try {
$this->authorize('delete', $this->database); $this->authorize('delete', $this->database);
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! verifyPasswordConfirmation($password, $this)) {
if (! Hash::check($password, Auth::user()->password)) { return;
$this->addError('password', 'The provided password is incorrect.');
return;
}
} }
$this->database->delete(); $this->database->delete();

View File

@@ -3,7 +3,6 @@
namespace App\Livewire\Project\Service; namespace App\Livewire\Project\Service;
use App\Models\Application; use App\Models\Application;
use App\Models\InstanceSettings;
use App\Models\LocalFileVolume; use App\Models\LocalFileVolume;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
@@ -16,8 +15,6 @@ use App\Models\StandaloneMysql;
use App\Models\StandalonePostgresql; use App\Models\StandalonePostgresql;
use App\Models\StandaloneRedis; use App\Models\StandaloneRedis;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
@@ -141,12 +138,8 @@ class FileStorage extends Component
{ {
$this->authorize('update', $this->resource); $this->authorize('update', $this->resource);
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! verifyPasswordConfirmation($password, $this)) {
if (! Hash::check($password, Auth::user()->password)) { return;
$this->addError('password', 'The provided password is incorrect.');
return;
}
} }
try { try {

View File

@@ -2,12 +2,9 @@
namespace App\Livewire\Project\Service; namespace App\Livewire\Project\Service;
use App\Models\InstanceSettings;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
use Spatie\Url\Url; use Spatie\Url\Url;
@@ -128,12 +125,8 @@ class ServiceApplicationView extends Component
try { try {
$this->authorize('delete', $this->application); $this->authorize('delete', $this->application);
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! verifyPasswordConfirmation($password, $this)) {
if (! Hash::check($password, Auth::user()->password)) { return;
$this->addError('password', 'The provided password is incorrect.');
return;
}
} }
$this->application->delete(); $this->application->delete();

View File

@@ -3,13 +3,10 @@
namespace App\Livewire\Project\Shared; namespace App\Livewire\Project\Shared;
use App\Jobs\DeleteResourceJob; use App\Jobs\DeleteResourceJob;
use App\Models\InstanceSettings;
use App\Models\Service; use App\Models\Service;
use App\Models\ServiceApplication; use App\Models\ServiceApplication;
use App\Models\ServiceDatabase; use App\Models\ServiceDatabase;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -93,12 +90,8 @@ class Danger extends Component
public function delete($password) public function delete($password)
{ {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! verifyPasswordConfirmation($password, $this)) {
if (! Hash::check($password, Auth::user()->password)) { return;
$this->addError('password', 'The provided password is incorrect.');
return;
}
} }
if (! $this->resource) { if (! $this->resource) {

View File

@@ -5,12 +5,9 @@ namespace App\Livewire\Project\Shared;
use App\Actions\Application\StopApplicationOneServer; use App\Actions\Application\StopApplicationOneServer;
use App\Actions\Docker\GetContainersStatus; use App\Actions\Docker\GetContainersStatus;
use App\Events\ApplicationStatusChanged; use App\Events\ApplicationStatusChanged;
use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use App\Models\StandaloneDocker; use App\Models\StandaloneDocker;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
use Visus\Cuid2\Cuid2; use Visus\Cuid2\Cuid2;
@@ -140,12 +137,8 @@ class Destination extends Component
public function removeServer(int $network_id, int $server_id, $password) public function removeServer(int $network_id, int $server_id, $password)
{ {
try { try {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! verifyPasswordConfirmation($password, $this)) {
if (! Hash::check($password, Auth::user()->password)) { return;
$this->addError('password', 'The provided password is incorrect.');
return;
}
} }
if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) { if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) {

View File

@@ -2,11 +2,8 @@
namespace App\Livewire\Project\Shared\Storages; namespace App\Livewire\Project\Shared\Storages;
use App\Models\InstanceSettings;
use App\Models\LocalPersistentVolume; use App\Models\LocalPersistentVolume;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class Show extends Component class Show extends Component
@@ -84,12 +81,8 @@ class Show extends Component
{ {
$this->authorize('update', $this->resource); $this->authorize('update', $this->resource);
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! verifyPasswordConfirmation($password, $this)) {
if (! Hash::check($password, Auth::user()->password)) { return;
$this->addError('password', 'The provided password is incorrect.');
return;
}
} }
$this->storage->delete(); $this->storage->delete();

View File

@@ -3,11 +3,8 @@
namespace App\Livewire\Server; namespace App\Livewire\Server;
use App\Actions\Server\DeleteServer; use App\Actions\Server\DeleteServer;
use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class Delete extends Component class Delete extends Component
@@ -29,12 +26,8 @@ class Delete extends Component
public function delete($password) public function delete($password)
{ {
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! verifyPasswordConfirmation($password, $this)) {
if (! Hash::check($password, Auth::user()->password)) { return;
$this->addError('password', 'The provided password is incorrect.');
return;
}
} }
try { try {
$this->authorize('delete', $this->server); $this->authorize('delete', $this->server);

View File

@@ -2,11 +2,8 @@
namespace App\Livewire\Server\Security; namespace App\Livewire\Server\Security;
use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
@@ -44,13 +41,9 @@ class TerminalAccess extends Component
throw new \Exception('Only team administrators and owners can modify terminal access.'); throw new \Exception('Only team administrators and owners can modify terminal access.');
} }
// Verify password unless two-step confirmation is disabled // Verify password
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! verifyPasswordConfirmation($password, $this)) {
if (! Hash::check($password, Auth::user()->password)) { return;
$this->addError('password', 'The provided password is incorrect.');
return;
}
} }
// Toggle the terminal setting // Toggle the terminal setting

View File

@@ -5,8 +5,6 @@ namespace App\Livewire\Settings;
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
use App\Models\Server; use App\Models\Server;
use App\Rules\ValidIpOrCidr; use App\Rules\ValidIpOrCidr;
use Auth;
use Hash;
use Livewire\Attributes\Validate; use Livewire\Attributes\Validate;
use Livewire\Component; use Livewire\Component;
@@ -157,9 +155,7 @@ class Advanced extends Component
public function toggleTwoStepConfirmation($password): bool public function toggleTwoStepConfirmation($password): bool
{ {
if (! Hash::check($password, Auth::user()->password)) { if (! verifyPasswordConfirmation($password, $this)) {
$this->addError('password', 'The provided password is incorrect.');
return false; return false;
} }

View File

@@ -2,10 +2,7 @@
namespace App\Livewire\Team; namespace App\Livewire\Team;
use App\Models\InstanceSettings;
use App\Models\User; use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Livewire\Component; use Livewire\Component;
class AdminView extends Component class AdminView extends Component
@@ -58,12 +55,8 @@ class AdminView extends Component
return redirect()->route('dashboard'); return redirect()->route('dashboard');
} }
if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! verifyPasswordConfirmation($password, $this)) {
if (! Hash::check($password, Auth::user()->password)) { return;
$this->addError('password', 'The provided password is incorrect.');
return;
}
} }
if (! auth()->user()->isInstanceAdmin()) { if (! auth()->user()->isInstanceAdmin()) {

View File

@@ -443,4 +443,13 @@ class User extends Authenticatable implements SendsEmail
&& $this->email_change_code_expires_at && $this->email_change_code_expires_at
&& Carbon::now()->lessThan($this->email_change_code_expires_at); && Carbon::now()->lessThan($this->email_change_code_expires_at);
} }
/**
* Check if the user has a password set.
* OAuth users are created without passwords.
*/
public function hasPassword(): bool
{
return ! empty($this->password);
}
} }

View File

@@ -33,6 +33,7 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
@@ -3308,3 +3309,57 @@ function formatContainerStatus(string $status): string
return str($status)->headline()->value(); return str($status)->headline()->value();
} }
} }
/**
* Check if password confirmation should be skipped.
* Returns true if:
* - Two-step confirmation is globally disabled
* - User has no password (OAuth users)
*
* Used by modal-confirmation.blade.php to determine if password step should be shown.
*
* @return bool True if password confirmation should be skipped
*/
function shouldSkipPasswordConfirmation(): bool
{
// Skip if two-step confirmation is globally disabled
if (data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) {
return true;
}
// Skip if user has no password (OAuth users)
if (! Auth::user()?->hasPassword()) {
return true;
}
return false;
}
/**
* Verify password for two-step confirmation.
* Skips verification if:
* - Two-step confirmation is globally disabled
* - User has no password (OAuth users)
*
* @param mixed $password The password to verify (may be array if skipped by frontend)
* @param \Livewire\Component|null $component Optional Livewire component to add errors to
* @return bool True if verification passed (or skipped), false if password is incorrect
*/
function verifyPasswordConfirmation(mixed $password, ?Livewire\Component $component = null): bool
{
// Skip if password confirmation should be skipped
if (shouldSkipPasswordConfirmation()) {
return true;
}
// Verify the password
if (! Hash::check($password, Auth::user()->password)) {
if ($component) {
$component->addError('password', 'The provided password is incorrect.');
}
return false;
}
return true;
}

View File

@@ -29,17 +29,23 @@
@php @php
use App\Models\InstanceSettings; use App\Models\InstanceSettings;
// Global setting to disable ALL two-step confirmation (text + password)
$disableTwoStepConfirmation = data_get(InstanceSettings::get(), 'disable_two_step_confirmation'); $disableTwoStepConfirmation = data_get(InstanceSettings::get(), 'disable_two_step_confirmation');
// Skip ONLY password confirmation for OAuth users (they have no password)
$skipPasswordConfirmation = shouldSkipPasswordConfirmation();
if ($temporaryDisableTwoStepConfirmation) { if ($temporaryDisableTwoStepConfirmation) {
$disableTwoStepConfirmation = false; $disableTwoStepConfirmation = false;
$skipPasswordConfirmation = false;
} }
// When password step is skipped, Step 2 becomes final - change button text from "Continue" to "Confirm"
$effectiveStep2ButtonText = ($skipPasswordConfirmation && $step2ButtonText === 'Continue') ? 'Confirm' : $step2ButtonText;
@endphp @endphp
<div {{ $ignoreWire ? 'wire:ignore' : '' }} x-data="{ <div {{ $ignoreWire ? 'wire:ignore' : '' }} x-data="{
modalOpen: false, modalOpen: false,
step: {{ empty($checkboxes) ? 2 : 1 }}, step: {{ empty($checkboxes) ? 2 : 1 }},
initialStep: {{ empty($checkboxes) ? 2 : 1 }}, initialStep: {{ empty($checkboxes) ? 2 : 1 }},
finalStep: {{ $confirmWithPassword && !$disableTwoStepConfirmation ? 3 : 2 }}, finalStep: {{ $confirmWithPassword && !$skipPasswordConfirmation ? 3 : 2 }},
deleteText: '', deleteText: '',
password: '', password: '',
actions: @js($actions), actions: @js($actions),
@@ -50,7 +56,7 @@
})(), })(),
userConfirmationText: '', userConfirmationText: '',
confirmWithText: @js($confirmWithText && !$disableTwoStepConfirmation), confirmWithText: @js($confirmWithText && !$disableTwoStepConfirmation),
confirmWithPassword: @js($confirmWithPassword && !$disableTwoStepConfirmation), confirmWithPassword: @js($confirmWithPassword && !$skipPasswordConfirmation),
submitAction: @js($submitAction), submitAction: @js($submitAction),
dispatchAction: @js($dispatchAction), dispatchAction: @js($dispatchAction),
passwordError: '', passwordError: '',
@@ -59,6 +65,7 @@
dispatchEventType: @js($dispatchEventType), dispatchEventType: @js($dispatchEventType),
dispatchEventMessage: @js($dispatchEventMessage), dispatchEventMessage: @js($dispatchEventMessage),
disableTwoStepConfirmation: @js($disableTwoStepConfirmation), disableTwoStepConfirmation: @js($disableTwoStepConfirmation),
skipPasswordConfirmation: @js($skipPasswordConfirmation),
resetModal() { resetModal() {
this.step = this.initialStep; this.step = this.initialStep;
this.deleteText = ''; this.deleteText = '';
@@ -68,7 +75,7 @@
$wire.$refresh(); $wire.$refresh();
}, },
step1ButtonText: @js($step1ButtonText), step1ButtonText: @js($step1ButtonText),
step2ButtonText: @js($step2ButtonText), step2ButtonText: @js($effectiveStep2ButtonText),
step3ButtonText: @js($step3ButtonText), step3ButtonText: @js($step3ButtonText),
validatePassword() { validatePassword() {
if (this.confirmWithPassword && !this.password) { if (this.confirmWithPassword && !this.password) {
@@ -92,10 +99,14 @@
const paramsMatch = this.submitAction.match(/\((.*?)\)/); const paramsMatch = this.submitAction.match(/\((.*?)\)/);
const params = paramsMatch ? paramsMatch[1].split(',').map(param => param.trim()) : []; const params = paramsMatch ? paramsMatch[1].split(',').map(param => param.trim()) : [];
if (this.confirmWithPassword) { // Always pass password parameter (empty string if password confirmation is skipped)
params.push(this.password); // This ensures consistent method signature for backend Livewire methods
params.push(this.confirmWithPassword ? this.password : '');
// Only pass selectedActions if there are checkboxes with selections
if (this.selectedActions.length > 0) {
params.push(this.selectedActions);
} }
params.push(this.selectedActions);
return $wire[methodName](...params) return $wire[methodName](...params)
.then(result => { .then(result => {
if (result === true) { if (result === true) {
@@ -316,7 +327,7 @@
if (dispatchEvent) { if (dispatchEvent) {
$wire.dispatch(dispatchEventType, dispatchEventMessage); $wire.dispatch(dispatchEventType, dispatchEventMessage);
} }
if (confirmWithPassword && !disableTwoStepConfirmation) { if (confirmWithPassword && !skipPasswordConfirmation) {
step++; step++;
} else { } else {
modalOpen = false; modalOpen = false;
@@ -330,7 +341,7 @@
</div> </div>
<!-- Step 3: Password confirmation --> <!-- Step 3: Password confirmation -->
@if (!$disableTwoStepConfirmation) @if (!$skipPasswordConfirmation)
<div x-show="step === 3 && confirmWithPassword"> <div x-show="step === 3 && confirmWithPassword">
<x-callout type="danger" title="Final Confirmation" class="mb-4"> <x-callout type="danger" title="Final Confirmation" class="mb-4">
Please enter your password to confirm this destructive action. Please enter your password to confirm this destructive action.