CluPilotCloud/app/Livewire/Concerns/ConfirmsPassword.php

95 lines
3.2 KiB
PHP

<?php
namespace App\Livewire\Concerns;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
/**
* A password re-entry gate in front of the dangerous parts of a page.
*
* Used by anything where holding a signed-in session should not be enough on
* its own: setting up two-factor authentication, and reading or changing stored
* credentials. The threat is not a stranger — it is a colleague, or anyone, at
* an unlocked machine.
*
* Stored in the SAME session key Laravel's own `password.confirm` middleware
* uses, so a confirmation made here also satisfies any framework route behind
* that middleware, and vice versa. Reinventing the marker would have produced
* two independent notions of "recently confirmed", one of which would drift.
*
* Rate-limited, because a confirmation form is a password oracle: without a
* limit it is an unauthenticated-feeling place to guess at a known account.
*/
trait ConfirmsPassword
{
public string $confirmablePassword = '';
/** How long one confirmation lasts. Laravel's own default. */
protected function confirmationWindow(): int
{
return (int) config('auth.password_timeout', 10800);
}
public function passwordRecentlyConfirmed(): bool
{
$at = (int) session('auth.password_confirmed_at', 0);
return $at > 0 && (time() - $at) < $this->confirmationWindow();
}
public function confirmPassword(): void
{
$key = 'confirm-password:'.auth()->id().'|'.request()->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
$this->addError('confirmablePassword', __('auth.throttle', [
'seconds' => RateLimiter::availableIn($key),
]));
return;
}
if (! Hash::check($this->confirmablePassword, auth()->user()->password)) {
RateLimiter::hit($key, 60);
$this->addError('confirmablePassword', __('admin_settings.password_wrong'));
$this->reset('confirmablePassword');
return;
}
RateLimiter::clear($key);
$this->reset('confirmablePassword');
// Regenerated first: a confirmation raises what this session can do, and
// a session id that was already known to someone else must not be the
// one that gets the extra authority.
session()->regenerate();
session(['auth.password_confirmed_at' => time()]);
}
/** Give up the confirmation early — leaving a page should not keep it open. */
public function forgetPasswordConfirmation(): void
{
session()->forget('auth.password_confirmed_at');
}
/**
* A value shown only in outline: the last four characters, and nothing else.
*
* Enough to tell two keys apart, useless to anyone reading over a shoulder
* or scrolling back through a screen recording.
*/
protected function outline(?string $value, int $keep = 4): ?string
{
if ($value === null || $value === '') {
return null;
}
return Str::length($value) <= $keep
? str_repeat('•', Str::length($value))
: str_repeat('•', 8).Str::substr($value, -$keep);
}
}