95 lines
3.2 KiB
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);
|
|
}
|
|
}
|