Migr游戏副本ing Legacy Passwords in a Laravel 12 + Next.js Stack: A Step-by-Step Guide
When rebuilding AustinsElite with a modern Laravel 12 frontend and Laravel 12 backend, we faced a common but critical challenge: thousands of users still had MD5-hashed passwords from the old system. These hashes weren’t secure by today’s standards, and we couldn’t just reset everyone’s password. Instead, we needed a seamless, secure migration path that preserved access while forcing an upgrade to modern hashing—without breaking trust.
This is how we did it, step by step.
Detecting Legacy Password Hashes in Laravel
The first step was identifying which users were still on legacy hashes. In Laravel 12, the Hash facade gives us a clean way to check hash types. We added a method to our User model to detect non-bcrypt hashes:
public function hasLegacyPassword(): bool
{
// Legacy hashes are 32 chars long (MD5) and don't start with $2y$ (bcrypt)
$password = $this->attributes['password'];
return strlen($password) === 32 && !str_starts_with($password, '$2y$');
}
During login, we hook into Laravel’s authentication flow. After validating credentials with Auth::attempt(), we check if the user has a legacy password:
if (Auth::attempt($credentials)) {
$user = Auth::user();
if ($user->hasLegacy游戏副本Password()) {
// Mark session for forced password reset
session(['force_password_reset' => true]);
return redirect()->route('password.change');
}
return redirect()->intended('/dashboard');
}
This way, only users with outdated hashes are redirected—everyone else logs in normally.
Implementing the Redirect Flow in Next.js
Our frontend is a standalone Next.js app communicating with Laravel via API routes. After login, the client checks the session state by calling a simple /api/session endpoint:
const res = await fetch('/api/session');
const data = await res.json();
if (data.forcePasswordReset) {
router.push('/change-password');
}
The /api/session route returns basic user state, including the forcePasswordReset flag set during login:
// routes/api.php
Route::get('/session', function () {
$user = Auth::user();
return response()->json([
'user' => $user ? $user->only(['id', 'name', 'email']) : null,
'forcePasswordReset' => session('force_password_reset', false)
]);
});
Once the flag is detected, Next.js redirects to a dedicated password change page. We made sure this route was protected—users can’t bypass it by manually navigating elsewhere. Middleware on the frontend enforces it:
useEffect(() => {
const checkSession = async () => {
const res = await fetch('/api/session');
const data = await res.json();
if (!data.user) {
router.push('/login');
} else if (data.forcePasswordReset && router.pathname !== '/change-password') {
router.push('/change-password');
}
};
checkSession();
}, [router]);
Forcing Password Reset with Clean UX and Security
The password change form is straightforward, but session handling is key. We wanted to avoid logging users out mid-flow, but also prevent them from accessing protected routes. Our solution: keep the session alive, block access via middleware (both server and client), and only clear the force_password_reset flag after a successful update.
Here’s the Laravel controller handling the update:
public function updatePassword(Request $request)
{
$request->validate([
'password' => ['required', 'confirmed', 'min:8']
]);
$user = Auth::user();
$user->password = Hash::make($request->password);
$user->save();
// Clear the reset flag
session()->forget('force_password_reset');
return response()->json(['message' => 'Password updated successfully']);
}
On success, the frontend removes the flag, clears local state, and redirects to the dashboard. We also added a logout link on the change password page—just in case users want to step away.
This flow ensured zero data loss, maintained security, and gave users a clear path forward. Since rollout, over 1,200 legacy accounts have been upgraded without a single support ticket related to login failure.
Migrating legacy systems isn’t glamorous, but getting it right means real users stay secure—and that’s worth the effort.