getAuthIdentifier(); // Whether this account has ever signed in anywhere, asked BEFORE the // row below exists. Warning somebody about the very first device they // have ever used is telling them they just did the thing they just did. $firstEver = ! UserDevice::query() ->where('guard', $guard) ->where('authenticatable_id', $id) ->exists(); // The cookie's token is reused when it is already there, even if it is // unknown for THIS identity: on a shared browser two people hold two // device rows against one token, and issuing a fresh token for the // second would orphan the first — whose owner then gets a new-device // warning on their own machine the next time they sign in. $token = $this->tokenFrom($request) ?? Str::random(48); $hash = hash('sha256', $token); $device = UserDevice::query() ->where('guard', $guard) ->where('authenticatable_id', $id) ->where('token_hash', $hash) ->first(); $isNew = $device === null; if ($isNew) { $device = new UserDevice([ 'guard' => $guard, 'authenticatable_id' => $id, 'token_hash' => $hash, ]); } // Recomputed every time, on a known device too: a browser upgrade // should relabel the row, not raise an alarm. It can only be safely // recomputed because the name identifies nothing. $device->name = DeviceName::from((string) $request->userAgent()); $device->last_ip = $request->ip(); $device->last_seen_at = now(); $device->save(); // Re-issued on every sign-in, known device included, so that somebody // who signs in regularly never falls off the end of the 400 days and // gets warned about the machine on their own desk. Cookie::queue(Cookie::make( self::COOKIE, $token, self::LIFETIME_DAYS * 24 * 60, secure: $request->isSecure(), httpOnly: true, sameSite: 'lax', )); return new DeviceOutcome( device: $device, isNew: $isNew, // A first device is new but is not news. shouldWarn: $isNew && ! $firstEver, ); } /** * Attach the current session to a device, and remember it by uuid. * * The uuid goes into the session payload rather than the row being keyed by * the session id, because Laravel rotates that id on sign-in and on every * regenerate(). A row keyed by it stops describing anything within a * request or two — and the list would then offer somebody sessions that no * longer exist alongside the one they are sitting in. */ public function attachSession(UserDevice $device, Request $request): LoginSession { $session = LoginSession::create([ 'device_id' => $device->id, 'session_id' => $request->hasSession() ? $request->session()->getId() : null, 'ip_address' => $request->ip(), 'last_seen_at' => now(), ]); if ($request->hasSession()) { $request->session()->put(LoginSession::KEY, $session->uuid); } return $session; } private function tokenFrom(Request $request): ?string { $value = $request->cookie(self::COOKIE); // Cookies are encrypted by the framework, so anything that survives // decryption was issued here — but a truncated or replayed value is // still not a token, and a lookup on nonsense must not match a row. return is_string($value) && strlen($value) >= 32 ? $value : null; } }