mirror of
https://github.com/projectsend/projectsend.git
synced 2026-09-20 10:33:20 +00:00
ff9ad10742
Reported by Ricardo Cazati, who had to turn compulsory two-factor off to get his colleagues working. An account provisioned by a provider carries a generated password nobody has ever seen. The password screen asked for the current one before it would set a new one, so those accounts could never have a password of their own — and enrolling in two-factor is behind a password confirmation, so they could not enrol either. With `TwoFactorEnforcement` set, the enforcement middleware sent them to enrol, enrolling sent them to confirm a password they do not have, and every other screen — including the one that would have given them one — redirected back. No way in and no way out. - The password screen asks for the current one only where there is one, and says "Set a password" otherwise. Setting it moves the account to `local`, the line NewPasswordController already writes when such an account resets its password: the hash is now what signs it in, and the settings screens read that off this column. - An LDAP account is refused outright rather than handed a password that signs nothing in — its password lives in the directory. - The enforcement middleware lets the password screen through, the way it already lets the confirm-password screen through, so the loop has an exit. - The confirm-password screen offers to set one instead of asking for a password that does not exist.
97 lines
3.9 KiB
PHP
97 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Settings;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Modules\Audit\Action;
|
|
use App\Modules\Audit\ActivityLogger;
|
|
use App\Modules\Identity\AuthSource;
|
|
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Validation\Rules\Password;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
|
|
class PasswordController extends Controller
|
|
{
|
|
/**
|
|
* Show the user's password settings page.
|
|
*/
|
|
public function edit(Request $request): Response
|
|
{
|
|
$user = $request->user();
|
|
assert($user !== null);
|
|
|
|
return Inertia::render('settings/password', [
|
|
'mustVerifyEmail' => $user instanceof MustVerifyEmail,
|
|
'status' => $request->session()->get('status'),
|
|
// Whether there is a password here at all. An account
|
|
// provisioned by a provider has a generated one nobody was
|
|
// ever told, so asking for "your current password" asks for
|
|
// something that does not exist — and until this, that was
|
|
// the only door to a password, which is the only way to reach
|
|
// two-factor enrolment. See update().
|
|
'has_local_password' => $user->auth_source === AuthSource::Local,
|
|
// A directory's password is not this installation's to change.
|
|
'managed_elsewhere' => $user->auth_source === AuthSource::Ldap,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Update the user's password.
|
|
*/
|
|
public function update(Request $request): RedirectResponse
|
|
{
|
|
$user = $request->user();
|
|
assert($user !== null);
|
|
|
|
// An LDAP account's password lives in the directory. Changing the
|
|
// hash here would change nothing anybody signs in with, so the
|
|
// honest answer is to refuse rather than to appear to work.
|
|
abort_if($user->auth_source === AuthSource::Ldap, 403);
|
|
|
|
$setsFirstPassword = $user->auth_source === AuthSource::Social;
|
|
|
|
$validated = $request->validate([
|
|
// Not asked of an account that has never had one: it signs in
|
|
// through a provider, and its stored hash is a generated
|
|
// string nobody has seen. Asking anyway left those accounts
|
|
// with no way to set a password — and so no way to enrol in
|
|
// two-factor, which an installation can make compulsory.
|
|
'current_password' => $setsFirstPassword ? ['nullable'] : ['required', 'current_password'],
|
|
'password' => ['required', Password::defaults(), 'confirmed'],
|
|
]);
|
|
|
|
$attributes = ['password' => Hash::make($validated['password'])];
|
|
|
|
// The same line NewPasswordController writes when a provider
|
|
// account resets its password, for the same reason: the hash is
|
|
// now what signs this account in, and `has_local_password` is read
|
|
// off this column all over the settings screens.
|
|
if ($setsFirstPassword) {
|
|
$attributes['auth_source'] = AuthSource::Local;
|
|
}
|
|
|
|
// forceFill, not update(): `auth_source` is guarded, so a mass
|
|
// assignment drops it silently — which left the account still
|
|
// reading as passwordless after it had a password.
|
|
$user->forceFill($attributes)->save();
|
|
|
|
// Changing a password is how someone reacts to a session they think
|
|
// is stolen, so it has to actually end that session. AuthenticateSession
|
|
// (registered on the web group) compares each request's stored
|
|
// password hash against the current one and logs out on mismatch;
|
|
// this re-stamps the current session so the person doing the change
|
|
// stays signed in while every other session falls over on its next
|
|
// request.
|
|
Auth::logoutOtherDevices($validated['password']);
|
|
|
|
app(ActivityLogger::class)->log(Action::PasswordUpdated, $user);
|
|
|
|
return back();
|
|
}
|
|
}
|