fix(auth): remove the unused Fortify profile route that bypassed the SSO email lock

PUT /user/profile-information changed the email with no SSO check and no
verification. The profile page has its own verified email change, so the
Fortify feature, its action and its binding are removed.
This commit is contained in:
Andras Bacsai
2026-09-30 18:10:18 +02:00
parent b1399d97df
commit 4f8bf46feb
4 changed files with 38 additions and 76 deletions
@@ -1,73 +0,0 @@
<?php
namespace App\Actions\Fortify;
use App\Models\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
{
/**
* Validate and update the given user's profile information.
*
* @param array<string, string> $input
*/
public function update(User $user, array $input): void
{
$changedFields = collect(['name', 'email'])
->filter(fn (string $field): bool => $user->{$field} !== $input[$field])
->values()
->all();
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => [
'required',
'string',
'email',
'max:255',
Rule::unique('users')->ignore($user->id),
],
])->validateWithBag('updateProfileInformation');
if (
$input['email'] !== $user->email &&
$user instanceof MustVerifyEmail
) {
$this->updateVerifiedUser($user, $input);
} else {
$user->fill([
'name' => $input['name'],
'email' => $input['email'],
])->save();
}
if ($changedFields !== []) {
auditLog('ui.user.profile_updated', [
'team_id' => $user->currentTeam()?->id,
'resource' => 'user',
'user_name' => $user->name,
'changed_fields' => $changedFields,
]);
}
}
/**
* Update the given verified user's profile information.
*
* @param array<string, string> $input
*/
protected function updateVerifiedUser(User $user, array $input): void
{
$user->fill([
'name' => $input['name'],
'email' => $input['email'],
'email_verified_at' => null,
])->save();
$user->sendEmailVerificationNotification();
}
}
-2
View File
@@ -5,7 +5,6 @@ namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Models\OauthSetting;
use App\Models\TeamInvitation;
use App\Models\User;
@@ -115,7 +114,6 @@ class FortifyServiceProvider extends ServiceProvider
});
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::confirmPasswordView(function () {
-1
View File
@@ -136,7 +136,6 @@ return [
Features::registration(),
Features::resetPasswords(),
// Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
Features::twoFactorAuthentication([
'confirm' => true,
@@ -0,0 +1,38 @@
<?php
use App\Models\InstanceSettings;
use App\Models\OauthIdentity;
use App\Models\User;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Route;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::forceCreate(['id' => 0]);
});
it('does not let a user change the email through the Fortify profile route', function (bool $hasSsoIdentity) {
$user = User::factory()->create(['email' => 'owner@example.com']);
if ($hasSsoIdentity) {
OauthIdentity::create([
'user_id' => $user->id,
'provider' => 'github',
'issuer' => 'github',
'provider_user_id' => '123',
'email' => 'owner@example.com',
]);
}
$this->actingAs($user)
->withoutMiddleware(VerifyCsrfToken::class)
->put('/user/profile-information', ['name' => 'Changed', 'email' => 'attacker@example.com']);
expect(Route::has('user-profile-information.update'))->toBeFalse()
->and($user->fresh()->email)->toBe('owner@example.com')
->and($user->fresh()->name)->not->toBe('Changed');
})->with([
'SSO user' => [true],
'password user' => [false],
]);