$request->user()]); } public function send(string $locale, Request $request): RedirectResponse { $user = $request->user(); $request->merge(['email' => Str::lower(trim((string) $request->input('email')))]); $validated = $request->validate([ 'email' => [ 'required', 'email', 'max:255', Rule::notIn([$user->email]), Rule::unique($user->getTable(), 'email')->ignore($user->id), ], ], [ 'email.not_in' => __('storefront.account.email_same'), 'email.unique' => __('storefront.account.email_taken'), ]); if (! $this->sendCode($request, $validated['email'])) { return back()->withInput()->withErrors(['email' => __('storefront.auth.too_many_codes')]); } return redirect()->route('account.email.code'); } public function code(string $locale, Request $request): View|RedirectResponse { $pending = $request->session()->get(self::SESSION_KEY); if (! $pending) { return redirect()->route('account.email.edit'); } return view('account.email-code', ['email' => $pending['email']]); } public function resend(string $locale, Request $request): RedirectResponse { $pending = $request->session()->get(self::SESSION_KEY); if (! $pending) { return redirect()->route('account.email.edit'); } if (! $this->sendCode($request, $pending['email'])) { return back()->withErrors(['code' => __('storefront.auth.too_many_codes')]); } return back()->with('status', __('storefront.auth.code_resent')); } public function verify(string $locale, Request $request, GuestOrderClaimer $claimer): RedirectResponse { $pending = $request->session()->get(self::SESSION_KEY); if (! $pending) { return redirect()->route('account.email.edit'); } $validated = $request->validate(['code' => ['required', 'digits:6']]); $valid = $pending['code_hash'] !== null && now()->timestamp < $pending['expires_at'] && Hash::check($validated['code'], $pending['code_hash']); if (! $valid) { // Too many wrong guesses burns the code; only "resend" helps then. $pending['attempts']++; if ($pending['attempts'] >= self::MAX_ATTEMPTS) { $pending['code_hash'] = null; } $request->session()->put(self::SESSION_KEY, $pending); return back()->withErrors(['code' => __('storefront.auth.invalid_code')]); } $user = $request->user(); // Someone may have signed up with this address since the code was sent. if ($user->newQuery()->where('email', $pending['email'])->whereKeyNot($user->id)->exists()) { $request->session()->forget(self::SESSION_KEY); return redirect()->route('account.email.edit') ->withErrors(['email' => __('storefront.account.email_taken')]); } $oldEmail = $user->email; $user->forceFill(['email' => $pending['email'], 'email_verified_at' => now()])->save(); // Lets the owner notice if someone else changed it from a hijacked session. Mail::to($oldEmail)->send(new EmailChangedNoticeMail($pending['email'])); // The code just proved they own the new address too. $claimer->claim($user); $request->session()->forget(self::SESSION_KEY); return redirect()->route('account')->with('status', __('storefront.account.email_changed')); } private function sendCode(Request $request, string $email): bool { $limiterKey = 'email-change:'.$request->user()->id; if (RateLimiter::tooManyAttempts($limiterKey, self::SEND_LIMIT)) { return false; } RateLimiter::hit($limiterKey, self::SEND_DECAY_SECONDS); $code = str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); $request->session()->put(self::SESSION_KEY, [ 'email' => $email, 'code_hash' => Hash::make($code), 'expires_at' => now()->addMinutes(self::EXPIRY_MINUTES)->timestamp, 'attempts' => 0, ]); Mail::to($email)->send(new EmailChangeCodeMail($code)); return true; } }