Skip to content

(Refactor) Swap fortify for native laravel functions for password resets #4802

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: development
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 0 additions & 66 deletions app/Actions/Fortify/ResetUserPassword.php

This file was deleted.

84 changes: 84 additions & 0 deletions app/Http/Controllers/Auth/NewPasswordController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author Roardom <roardom@protonmail.com>
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
*/

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\Group;
use App\Models\User;
use App\Services\Unit3dAnnounce;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Validation\Rules\Password as RulesPassword;

class NewPasswordController extends Controller
{
/**
* Create new password.
*/
public function create(string $token): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return view('auth.reset-password', ['token' => $token]);
}

/**
* Store a new password.
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => RulesPassword::min(12)->mixedCase()->letters()->numbers()->uncompromised(),
]);

$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function (User $user, string $password): void {
$user->forceFill([
'password' => Hash::make($password)
]);

$validatingGroup = cache()->rememberForever('validating_group', fn () => Group::query()->where('slug', '=', 'validating')->pluck('id'));
$memberGroup = cache()->rememberForever('member_group', fn () => Group::query()->where('slug', '=', 'user')->pluck('id'));

if ($user->group_id === $validatingGroup[0]) {
$user->group_id = $memberGroup[0];

cache()->forget('user:'.$user->passkey);

Unit3dAnnounce::addUser($user);
}

if (!$user->hasVerifiedEmail()) {
$user->markEmailAsVerified();
}

$user->save();

$user->passwordResetHistories()->create();

event(new PasswordReset($user));
}
);

return $status === Password::PasswordReset
? redirect()->route('login')->with('status', __($status))
: back()->withErrors(['email' => [__($status)]]);
}
}
48 changes: 48 additions & 0 deletions app/Http/Controllers/Auth/PasswordResetLinkController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

/**
* NOTICE OF LICENSE.
*
* UNIT3D Community Edition is open-sourced software licensed under the GNU Affero General Public License v3.0
* The details is bundled with this project in the file LICENSE.txt.
*
* @project UNIT3D Community Edition
*
* @author Roardom <roardom@protonmail.com>
* @license https://www.gnu.org/licenses/agpl-3.0.en.html/ GNU Affero General Public License v3.0
*/

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;

class PasswordResetLinkController extends Controller
{
/**
* Show form to submit to receive new password reset link.
*/
public function create(): \Illuminate\Contracts\View\Factory|\Illuminate\View\View
{
return view('auth.forgot-password');
}

/**
* Send a new password reset link.
*/
public function store(Request $request): \Illuminate\Http\RedirectResponse
{
$request->validate(['email' => 'required|email']);

$_status = Password::sendResetLink(
$request->only('email')
);

// Return successful status regardless of if the user exists or if they're throttled or not.
// (Account enumeration)
return back()->with(['status' => __('passwords.sent')]);
}
}
13 changes: 0 additions & 13 deletions app/Providers/FortifyServiceProvider.php
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<?php

declare(strict_types=1);
Expand All @@ -17,7 +17,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\FailedLoginAttempt;
Expand All @@ -28,7 +27,6 @@
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rule;
Expand All @@ -37,8 +35,6 @@
use Laravel\Fortify\Contracts\RegisterViewResponse;
use Laravel\Fortify\Contracts\VerifyEmailResponse;
use Laravel\Fortify\Fortify;
use Laravel\Fortify\Http\Responses\FailedPasswordResetLinkRequestResponse;
use Laravel\Fortify\Http\Responses\SuccessfulPasswordResetLinkRequestResponse;

class FortifyServiceProvider extends ServiceProvider
{
Expand Down Expand Up @@ -81,7 +77,7 @@
if ($rootUrlOverride = config('unit3d.root_url_override')) {
$url = redirect()->getIntendedUrl();

return $url === null ? $rootUrlOverride : redirect(

Check failure on line 80 in app/Providers/FortifyServiceProvider.php

View workflow job for this annotation

GitHub Actions / php 8.4 on ubuntu-22.04

Method Laravel\Fortify\Contracts\LoginResponse@anonymous/app/Providers/FortifyServiceProvider.php:47::toResponse() should return Illuminate\Http\RedirectResponse but returns Illuminate\Http\RedirectResponse|true.
rtrim(
rtrim($rootUrlOverride, '/')
.parse_url($url, PHP_URL_PATH)
Expand All @@ -97,7 +93,7 @@

// Handle redirects before the registration form is shown
$this->app->instance(RegisterViewResponse::class, new class () implements RegisterViewResponse {
public function toResponse($request): \Illuminate\Http\RedirectResponse|\Illuminate\View\View

Check failure on line 96 in app/Providers/FortifyServiceProvider.php

View workflow job for this annotation

GitHub Actions / php 8.4 on ubuntu-22.04

Method Laravel\Fortify\Contracts\RegisterViewResponse@anonymous/app/Providers/FortifyServiceProvider.php:95::toResponse() never returns Illuminate\Http\RedirectResponse so it can be removed from the return type.
{
if ($request->missing('code')) {
return view('auth.register');
Expand All @@ -108,7 +104,7 @@
});

$this->app->instance(VerifyEmailResponse::class, new class () implements VerifyEmailResponse {
public function toResponse($request): \Illuminate\Http\RedirectResponse|\Illuminate\View\View

Check failure on line 107 in app/Providers/FortifyServiceProvider.php

View workflow job for this annotation

GitHub Actions / php 8.4 on ubuntu-22.04

Method Laravel\Fortify\Contracts\VerifyEmailResponse@anonymous/app/Providers/FortifyServiceProvider.php:106::toResponse() never returns Illuminate\View\View so it can be removed from the return type.
{
$bannedGroup = cache()->rememberForever('banned_group', fn () => Group::query()->where('slug', '=', 'banned')->pluck('id'));
$validatingGroup = cache()->rememberForever('validating_group', fn () => Group::query()->where('slug', '=', 'validating')->pluck('id'));
Expand Down Expand Up @@ -142,8 +138,6 @@
->withErrors(trans('auth.activation-error'));
}
});

$this->app->extend(FailedPasswordResetLinkRequestResponse::class, fn () => new SuccessfulPasswordResetLinkRequestResponse(Password::RESET_LINK_SENT));
}

/**
Expand All @@ -155,23 +149,16 @@
RateLimiter::for('fortify-login-get', fn (Request $request) => Limit::perMinute(5)->by('fortify-login'.$request->ip()));
RateLimiter::for('fortify-register-get', fn (Request $request) => Limit::perMinute(5)->by('fortify-register'.$request->ip()));
RateLimiter::for('fortify-register-post', fn (Request $request) => Limit::perMinute(5)->by('fortify-register'.$request->ip()));
RateLimiter::for('fortify-forgot-password-get', fn (Request $request) => Limit::perMinute(5)->by('fortify-forgot-password'.$request->ip()));
RateLimiter::for('fortify-forgot-password-post', fn (Request $request) => Limit::perMinute(5)->by('fortify-forgot-password'.$request->ip()));
RateLimiter::for('fortify-reset-password-get', fn (Request $request) => Limit::perMinute(5)->by('fortify-reset-password'.$request->ip()));
RateLimiter::for('fortify-reset-password-post', fn (Request $request) => Limit::perMinute(5)->by('fortify-reset-password'.$request->ip()));
RateLimiter::for('two-factor', fn (Request $request) => Limit::perMinute(5)->by('fortify-two-factor'.$request->session()->get('login.id')));

Fortify::loginView(fn () => view('auth.login'));
Fortify::requestPasswordResetLinkView(fn () => view('auth.passwords.email'));
Fortify::resetPasswordView(fn (Request $request) => view('auth.passwords.reset', ['request' => $request]));
Fortify::confirmPasswordView(fn () => view('auth.confirm-password'));
Fortify::twoFactorChallengeView(fn () => view('auth.two-factor-challenge'));
Fortify::verifyEmailView(fn () => view('auth.verify-email'));

Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);

Fortify::authenticateUsing(function (Request $request): \Illuminate\Database\Eloquent\Model {
$request->validate([
Expand Down
2 changes: 2 additions & 0 deletions app/Providers/RouteServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ protected function configureRateLimiting(): void
RateLimiter::for('search', fn (Request $request): Limit => Limit::perMinute(100)->by('search:'.$request->user()->id));
RateLimiter::for('tmdb', fn (): Limit => Limit::perSecond(2));
RateLimiter::for('igdb', fn (): Limit => Limit::perSecond(2));
RateLimiter::for('forgot-password', fn (Request $request) => Limit::perMinute(5)->by('forgot-password'.$request->ip()));
RateLimiter::for('reset-password', fn (Request $request) => Limit::perMinute(5)->by('reset-password'.$request->ip()));
}

protected function removeIndexPhpFromUrl(): void
Expand Down
1 change: 0 additions & 1 deletion config/fortify.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@

'features' => [
Features::registration(),
Features::resetPasswords(),
Features::emailVerification(),
Features::updateProfileInformation(),
Features::updatePasswords(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class="auth-form__form"
action="{{ route('password.update') }}"
>
@csrf
<input type="hidden" name="token" value="{{ request()->route('token') }}" />
<input type="hidden" name="token" value="{{ $token }}" />
<a class="auth-form__branding" href="{{ route('home.index') }}">
<i class="fal fa-tv-retro"></i>
<span class="auth-form__site-logo">{{ \config('other.title') }}</span>
Expand Down
24 changes: 6 additions & 18 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\URL;
use Laravel\Fortify\Http\Controllers\AuthenticatedSessionController;
use Laravel\Fortify\Http\Controllers\NewPasswordController;
use Laravel\Fortify\Http\Controllers\PasswordResetLinkController;
use Laravel\Fortify\Http\Controllers\RegisteredUserController;
use Laravel\Fortify\RoutePath;

Expand Down Expand Up @@ -58,22 +56,6 @@

Route::post(RoutePath::for('register', '/register'), [RegisteredUserController::class, 'store'])
->middleware(['throttle:'.config('fortify.limiters.fortify-register-post')]);

Route::get(RoutePath::for('password.request', '/forgot-password'), [PasswordResetLinkController::class, 'create'])
->middleware(['throttle:'.config('fortify.limiters.fortify-forgot-password-get')])
->name('password.request');

Route::get(RoutePath::for('password.reset', '/reset-password/{token}'), [NewPasswordController::class, 'create'])
->middleware(['throttle:'.config('fortify.limiters.fortify-reset-password-get')])
->name('password.reset');

Route::post(RoutePath::for('password.email', '/forgot-password'), [PasswordResetLinkController::class, 'store'])
->middleware(['throttle:'.config('fortify.limiters.fortify-forgot-password-post')])
->name('password.email');

Route::post(RoutePath::for('password.update', '/reset-password'), [NewPasswordController::class, 'store'])
->middleware(['throttle:'.config('fortify.limiters.fortify-reset-password-post')])
->name('password.update');
});

/*
Expand All @@ -86,6 +68,12 @@
Route::get('/application', [App\Http\Controllers\Auth\ApplicationController::class, 'create'])->name('application.create');
Route::post('/application', [App\Http\Controllers\Auth\ApplicationController::class, 'store'])->name('application.store');

// Password resets
Route::get('/forgot-password', [App\Http\Controllers\Auth\PasswordResetLinkController::class, 'create'])->middleware('throttle:forgot-password')->name('password.request');
Route::post('/forgot-password', [App\Http\Controllers\Auth\PasswordResetLinkController::class, 'store'])->middleware(['throttle:forgot-password'])->name('password.email');
Route::get('/reset-password/{token}', [App\Http\Controllers\Auth\NewPasswordController::class, 'create'])->middleware('throttle:reset-password')->name('password.reset');
Route::post('/reset-password', [App\Http\Controllers\Auth\NewPasswordController::class, 'store'])->middleware('throttle:reset-password')->name('password.update');

// This redirect must be kept until all invite emails that use the old syntax have expired
// Hack so that Fortify can be used (allows query parameters but not route parameters)
Route::get('/register/{code?}', fn (string $code) => to_route('register', ['code' => $code]));
Expand Down
Loading