SessionGuard.php 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775
  1. <?php
  2. namespace Illuminate\Auth;
  3. use RuntimeException;
  4. use Illuminate\Support\Str;
  5. use Illuminate\Support\Facades\Hash;
  6. use Illuminate\Support\Traits\Macroable;
  7. use Illuminate\Contracts\Session\Session;
  8. use Illuminate\Contracts\Auth\UserProvider;
  9. use Illuminate\Contracts\Events\Dispatcher;
  10. use Illuminate\Contracts\Auth\StatefulGuard;
  11. use Symfony\Component\HttpFoundation\Request;
  12. use Illuminate\Contracts\Auth\SupportsBasicAuth;
  13. use Illuminate\Contracts\Cookie\QueueingFactory as CookieJar;
  14. use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
  15. use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
  16. class SessionGuard implements StatefulGuard, SupportsBasicAuth
  17. {
  18. use GuardHelpers, Macroable;
  19. /**
  20. * The name of the Guard. Typically "session".
  21. *
  22. * Corresponds to guard name in authentication configuration.
  23. *
  24. * @var string
  25. */
  26. protected $name;
  27. /**
  28. * The user we last attempted to retrieve.
  29. *
  30. * @var \Illuminate\Contracts\Auth\Authenticatable
  31. */
  32. protected $lastAttempted;
  33. /**
  34. * Indicates if the user was authenticated via a recaller cookie.
  35. *
  36. * @var bool
  37. */
  38. protected $viaRemember = false;
  39. /**
  40. * The session used by the guard.
  41. *
  42. * @var \Illuminate\Contracts\Session\Session
  43. */
  44. protected $session;
  45. /**
  46. * The Illuminate cookie creator service.
  47. *
  48. * @var \Illuminate\Contracts\Cookie\QueueingFactory
  49. */
  50. protected $cookie;
  51. /**
  52. * The request instance.
  53. *
  54. * @var \Symfony\Component\HttpFoundation\Request
  55. */
  56. protected $request;
  57. /**
  58. * The event dispatcher instance.
  59. *
  60. * @var \Illuminate\Contracts\Events\Dispatcher
  61. */
  62. protected $events;
  63. /**
  64. * Indicates if the logout method has been called.
  65. *
  66. * @var bool
  67. */
  68. protected $loggedOut = false;
  69. /**
  70. * Indicates if a token user retrieval has been attempted.
  71. *
  72. * @var bool
  73. */
  74. protected $recallAttempted = false;
  75. /**
  76. * Create a new authentication guard.
  77. *
  78. * @param string $name
  79. * @param \Illuminate\Contracts\Auth\UserProvider $provider
  80. * @param \Illuminate\Contracts\Session\Session $session
  81. * @param \Symfony\Component\HttpFoundation\Request|null $request
  82. * @return void
  83. */
  84. public function __construct($name,
  85. UserProvider $provider,
  86. Session $session,
  87. Request $request = null)
  88. {
  89. $this->name = $name;
  90. $this->session = $session;
  91. $this->request = $request;
  92. $this->provider = $provider;
  93. }
  94. /**
  95. * Get the currently authenticated user.
  96. *
  97. * @return \Illuminate\Contracts\Auth\Authenticatable|null
  98. */
  99. public function user()
  100. {
  101. if ($this->loggedOut) {
  102. return;
  103. }
  104. // If we've already retrieved the user for the current request we can just
  105. // return it back immediately. We do not want to fetch the user data on
  106. // every call to this method because that would be tremendously slow.
  107. if (! is_null($this->user)) {
  108. return $this->user;
  109. }
  110. $id = $this->session->get($this->getName());
  111. // First we will try to load the user using the identifier in the session if
  112. // one exists. Otherwise we will check for a "remember me" cookie in this
  113. // request, and if one exists, attempt to retrieve the user using that.
  114. if (! is_null($id)) {
  115. if ($this->user = $this->provider->retrieveById($id)) {
  116. $this->fireAuthenticatedEvent($this->user);
  117. }
  118. }
  119. // If the user is null, but we decrypt a "recaller" cookie we can attempt to
  120. // pull the user data on that cookie which serves as a remember cookie on
  121. // the application. Once we have a user we can return it to the caller.
  122. $recaller = $this->recaller();
  123. if (is_null($this->user) && ! is_null($recaller)) {
  124. $this->user = $this->userFromRecaller($recaller);
  125. if ($this->user) {
  126. $this->updateSession($this->user->getAuthIdentifier());
  127. $this->fireLoginEvent($this->user, true);
  128. }
  129. }
  130. return $this->user;
  131. }
  132. /**
  133. * Pull a user from the repository by its "remember me" cookie token.
  134. *
  135. * @param \Illuminate\Auth\Recaller $recaller
  136. * @return mixed
  137. */
  138. protected function userFromRecaller($recaller)
  139. {
  140. if (! $recaller->valid() || $this->recallAttempted) {
  141. return;
  142. }
  143. // If the user is null, but we decrypt a "recaller" cookie we can attempt to
  144. // pull the user data on that cookie which serves as a remember cookie on
  145. // the application. Once we have a user we can return it to the caller.
  146. $this->recallAttempted = true;
  147. $this->viaRemember = ! is_null($user = $this->provider->retrieveByToken(
  148. $recaller->id(), $recaller->token()
  149. ));
  150. return $user;
  151. }
  152. /**
  153. * Get the decrypted recaller cookie for the request.
  154. *
  155. * @return \Illuminate\Auth\Recaller|null
  156. */
  157. protected function recaller()
  158. {
  159. if (is_null($this->request)) {
  160. return;
  161. }
  162. if ($recaller = $this->request->cookies->get($this->getRecallerName())) {
  163. return new Recaller($recaller);
  164. }
  165. }
  166. /**
  167. * Get the ID for the currently authenticated user.
  168. *
  169. * @return int|null
  170. */
  171. public function id()
  172. {
  173. if ($this->loggedOut) {
  174. return;
  175. }
  176. return $this->user()
  177. ? $this->user()->getAuthIdentifier()
  178. : $this->session->get($this->getName());
  179. }
  180. /**
  181. * Log a user into the application without sessions or cookies.
  182. *
  183. * @param array $credentials
  184. * @return bool
  185. */
  186. public function once(array $credentials = [])
  187. {
  188. $this->fireAttemptEvent($credentials);
  189. if ($this->validate($credentials)) {
  190. $this->setUser($this->lastAttempted);
  191. return true;
  192. }
  193. return false;
  194. }
  195. /**
  196. * Log the given user ID into the application without sessions or cookies.
  197. *
  198. * @param mixed $id
  199. * @return \Illuminate\Contracts\Auth\Authenticatable|false
  200. */
  201. public function onceUsingId($id)
  202. {
  203. if (! is_null($user = $this->provider->retrieveById($id))) {
  204. $this->setUser($user);
  205. return $user;
  206. }
  207. return false;
  208. }
  209. /**
  210. * Validate a user's credentials.
  211. *
  212. * @param array $credentials
  213. * @return bool
  214. */
  215. public function validate(array $credentials = [])
  216. {
  217. $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
  218. return $this->hasValidCredentials($user, $credentials);
  219. }
  220. /**
  221. * Attempt to authenticate using HTTP Basic Auth.
  222. *
  223. * @param string $field
  224. * @param array $extraConditions
  225. * @return \Symfony\Component\HttpFoundation\Response|null
  226. */
  227. public function basic($field = 'email', $extraConditions = [])
  228. {
  229. if ($this->check()) {
  230. return;
  231. }
  232. // If a username is set on the HTTP basic request, we will return out without
  233. // interrupting the request lifecycle. Otherwise, we'll need to generate a
  234. // request indicating that the given credentials were invalid for login.
  235. if ($this->attemptBasic($this->getRequest(), $field, $extraConditions)) {
  236. return;
  237. }
  238. return $this->failedBasicResponse();
  239. }
  240. /**
  241. * Perform a stateless HTTP Basic login attempt.
  242. *
  243. * @param string $field
  244. * @param array $extraConditions
  245. * @return \Symfony\Component\HttpFoundation\Response|null
  246. */
  247. public function onceBasic($field = 'email', $extraConditions = [])
  248. {
  249. $credentials = $this->basicCredentials($this->getRequest(), $field);
  250. if (! $this->once(array_merge($credentials, $extraConditions))) {
  251. return $this->failedBasicResponse();
  252. }
  253. }
  254. /**
  255. * Attempt to authenticate using basic authentication.
  256. *
  257. * @param \Symfony\Component\HttpFoundation\Request $request
  258. * @param string $field
  259. * @param array $extraConditions
  260. * @return bool
  261. */
  262. protected function attemptBasic(Request $request, $field, $extraConditions = [])
  263. {
  264. if (! $request->getUser()) {
  265. return false;
  266. }
  267. return $this->attempt(array_merge(
  268. $this->basicCredentials($request, $field), $extraConditions
  269. ));
  270. }
  271. /**
  272. * Get the credential array for a HTTP Basic request.
  273. *
  274. * @param \Symfony\Component\HttpFoundation\Request $request
  275. * @param string $field
  276. * @return array
  277. */
  278. protected function basicCredentials(Request $request, $field)
  279. {
  280. return [$field => $request->getUser(), 'password' => $request->getPassword()];
  281. }
  282. /**
  283. * Get the response for basic authentication.
  284. *
  285. * @return void
  286. * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
  287. */
  288. protected function failedBasicResponse()
  289. {
  290. throw new UnauthorizedHttpException('Basic', 'Invalid credentials.');
  291. }
  292. /**
  293. * Attempt to authenticate a user using the given credentials.
  294. *
  295. * @param array $credentials
  296. * @param bool $remember
  297. * @return bool
  298. */
  299. public function attempt(array $credentials = [], $remember = false)
  300. {
  301. $this->fireAttemptEvent($credentials, $remember);
  302. $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
  303. // If an implementation of UserInterface was returned, we'll ask the provider
  304. // to validate the user against the given credentials, and if they are in
  305. // fact valid we'll log the users into the application and return true.
  306. if ($this->hasValidCredentials($user, $credentials)) {
  307. $this->login($user, $remember);
  308. return true;
  309. }
  310. // If the authentication attempt fails we will fire an event so that the user
  311. // may be notified of any suspicious attempts to access their account from
  312. // an unrecognized user. A developer may listen to this event as needed.
  313. $this->fireFailedEvent($user, $credentials);
  314. return false;
  315. }
  316. /**
  317. * Determine if the user matches the credentials.
  318. *
  319. * @param mixed $user
  320. * @param array $credentials
  321. * @return bool
  322. */
  323. protected function hasValidCredentials($user, $credentials)
  324. {
  325. return ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
  326. }
  327. /**
  328. * Log the given user ID into the application.
  329. *
  330. * @param mixed $id
  331. * @param bool $remember
  332. * @return \Illuminate\Contracts\Auth\Authenticatable|false
  333. */
  334. public function loginUsingId($id, $remember = false)
  335. {
  336. if (! is_null($user = $this->provider->retrieveById($id))) {
  337. $this->login($user, $remember);
  338. return $user;
  339. }
  340. return false;
  341. }
  342. /**
  343. * Log a user into the application.
  344. *
  345. * @param \Illuminate\Contracts\Auth\Authenticatable $user
  346. * @param bool $remember
  347. * @return void
  348. */
  349. public function login(AuthenticatableContract $user, $remember = false)
  350. {
  351. $this->updateSession($user->getAuthIdentifier());
  352. // If the user should be permanently "remembered" by the application we will
  353. // queue a permanent cookie that contains the encrypted copy of the user
  354. // identifier. We will then decrypt this later to retrieve the users.
  355. if ($remember) {
  356. $this->ensureRememberTokenIsSet($user);
  357. $this->queueRecallerCookie($user);
  358. }
  359. // If we have an event dispatcher instance set we will fire an event so that
  360. // any listeners will hook into the authentication events and run actions
  361. // based on the login and logout events fired from the guard instances.
  362. $this->fireLoginEvent($user, $remember);
  363. $this->setUser($user);
  364. }
  365. /**
  366. * Update the session with the given ID.
  367. *
  368. * @param string $id
  369. * @return void
  370. */
  371. protected function updateSession($id)
  372. {
  373. $this->session->put($this->getName(), $id);
  374. $this->session->migrate(true);
  375. }
  376. /**
  377. * Create a new "remember me" token for the user if one doesn't already exist.
  378. *
  379. * @param \Illuminate\Contracts\Auth\Authenticatable $user
  380. * @return void
  381. */
  382. protected function ensureRememberTokenIsSet(AuthenticatableContract $user)
  383. {
  384. if (empty($user->getRememberToken())) {
  385. $this->cycleRememberToken($user);
  386. }
  387. }
  388. /**
  389. * Queue the recaller cookie into the cookie jar.
  390. *
  391. * @param \Illuminate\Contracts\Auth\Authenticatable $user
  392. * @return void
  393. */
  394. protected function queueRecallerCookie(AuthenticatableContract $user)
  395. {
  396. $this->getCookieJar()->queue($this->createRecaller(
  397. $user->getAuthIdentifier().'|'.$user->getRememberToken().'|'.$user->getAuthPassword()
  398. ));
  399. }
  400. /**
  401. * Create a "remember me" cookie for a given ID.
  402. *
  403. * @param string $value
  404. * @return \Symfony\Component\HttpFoundation\Cookie
  405. */
  406. protected function createRecaller($value)
  407. {
  408. return $this->getCookieJar()->forever($this->getRecallerName(), $value);
  409. }
  410. /**
  411. * Log the user out of the application.
  412. *
  413. * @return void
  414. */
  415. public function logout()
  416. {
  417. $user = $this->user();
  418. // If we have an event dispatcher instance, we can fire off the logout event
  419. // so any further processing can be done. This allows the developer to be
  420. // listening for anytime a user signs out of this application manually.
  421. $this->clearUserDataFromStorage();
  422. if (! is_null($this->user)) {
  423. $this->cycleRememberToken($user);
  424. }
  425. if (isset($this->events)) {
  426. $this->events->dispatch(new Events\Logout($user));
  427. }
  428. // Once we have fired the logout event we will clear the users out of memory
  429. // so they are no longer available as the user is no longer considered as
  430. // being signed into this application and should not be available here.
  431. $this->user = null;
  432. $this->loggedOut = true;
  433. }
  434. /**
  435. * Remove the user data from the session and cookies.
  436. *
  437. * @return void
  438. */
  439. protected function clearUserDataFromStorage()
  440. {
  441. $this->session->remove($this->getName());
  442. if (! is_null($this->recaller())) {
  443. $this->getCookieJar()->queue($this->getCookieJar()
  444. ->forget($this->getRecallerName()));
  445. }
  446. }
  447. /**
  448. * Refresh the "remember me" token for the user.
  449. *
  450. * @param \Illuminate\Contracts\Auth\Authenticatable $user
  451. * @return void
  452. */
  453. protected function cycleRememberToken(AuthenticatableContract $user)
  454. {
  455. $user->setRememberToken($token = Str::random(60));
  456. $this->provider->updateRememberToken($user, $token);
  457. }
  458. /**
  459. * Invalidate other sessions for the current user.
  460. *
  461. * The application must be using the AuthenticateSession middleware.
  462. *
  463. * @param string $password
  464. * @param string $attribute
  465. * @return null|bool
  466. */
  467. public function logoutOtherDevices($password, $attribute = 'password')
  468. {
  469. if (! $this->user()) {
  470. return;
  471. }
  472. return tap($this->user()->forceFill([
  473. $attribute => Hash::make($password),
  474. ]))->save();
  475. }
  476. /**
  477. * Register an authentication attempt event listener.
  478. *
  479. * @param mixed $callback
  480. * @return void
  481. */
  482. public function attempting($callback)
  483. {
  484. if (isset($this->events)) {
  485. $this->events->listen(Events\Attempting::class, $callback);
  486. }
  487. }
  488. /**
  489. * Fire the attempt event with the arguments.
  490. *
  491. * @param array $credentials
  492. * @param bool $remember
  493. * @return void
  494. */
  495. protected function fireAttemptEvent(array $credentials, $remember = false)
  496. {
  497. if (isset($this->events)) {
  498. $this->events->dispatch(new Events\Attempting(
  499. $credentials, $remember
  500. ));
  501. }
  502. }
  503. /**
  504. * Fire the login event if the dispatcher is set.
  505. *
  506. * @param \Illuminate\Contracts\Auth\Authenticatable $user
  507. * @param bool $remember
  508. * @return void
  509. */
  510. protected function fireLoginEvent($user, $remember = false)
  511. {
  512. if (isset($this->events)) {
  513. $this->events->dispatch(new Events\Login($user, $remember));
  514. }
  515. }
  516. /**
  517. * Fire the authenticated event if the dispatcher is set.
  518. *
  519. * @param \Illuminate\Contracts\Auth\Authenticatable $user
  520. * @return void
  521. */
  522. protected function fireAuthenticatedEvent($user)
  523. {
  524. if (isset($this->events)) {
  525. $this->events->dispatch(new Events\Authenticated($user));
  526. }
  527. }
  528. /**
  529. * Fire the failed authentication attempt event with the given arguments.
  530. *
  531. * @param \Illuminate\Contracts\Auth\Authenticatable|null $user
  532. * @param array $credentials
  533. * @return void
  534. */
  535. protected function fireFailedEvent($user, array $credentials)
  536. {
  537. if (isset($this->events)) {
  538. $this->events->dispatch(new Events\Failed($user, $credentials));
  539. }
  540. }
  541. /**
  542. * Get the last user we attempted to authenticate.
  543. *
  544. * @return \Illuminate\Contracts\Auth\Authenticatable
  545. */
  546. public function getLastAttempted()
  547. {
  548. return $this->lastAttempted;
  549. }
  550. /**
  551. * Get a unique identifier for the auth session value.
  552. *
  553. * @return string
  554. */
  555. public function getName()
  556. {
  557. return 'login_'.$this->name.'_'.sha1(static::class);
  558. }
  559. /**
  560. * Get the name of the cookie used to store the "recaller".
  561. *
  562. * @return string
  563. */
  564. public function getRecallerName()
  565. {
  566. return 'remember_'.$this->name.'_'.sha1(static::class);
  567. }
  568. /**
  569. * Determine if the user was authenticated via "remember me" cookie.
  570. *
  571. * @return bool
  572. */
  573. public function viaRemember()
  574. {
  575. return $this->viaRemember;
  576. }
  577. /**
  578. * Get the cookie creator instance used by the guard.
  579. *
  580. * @return \Illuminate\Contracts\Cookie\QueueingFactory
  581. *
  582. * @throws \RuntimeException
  583. */
  584. public function getCookieJar()
  585. {
  586. if (! isset($this->cookie)) {
  587. throw new RuntimeException('Cookie jar has not been set.');
  588. }
  589. return $this->cookie;
  590. }
  591. /**
  592. * Set the cookie creator instance used by the guard.
  593. *
  594. * @param \Illuminate\Contracts\Cookie\QueueingFactory $cookie
  595. * @return void
  596. */
  597. public function setCookieJar(CookieJar $cookie)
  598. {
  599. $this->cookie = $cookie;
  600. }
  601. /**
  602. * Get the event dispatcher instance.
  603. *
  604. * @return \Illuminate\Contracts\Events\Dispatcher
  605. */
  606. public function getDispatcher()
  607. {
  608. return $this->events;
  609. }
  610. /**
  611. * Set the event dispatcher instance.
  612. *
  613. * @param \Illuminate\Contracts\Events\Dispatcher $events
  614. * @return void
  615. */
  616. public function setDispatcher(Dispatcher $events)
  617. {
  618. $this->events = $events;
  619. }
  620. /**
  621. * Get the session store used by the guard.
  622. *
  623. * @return \Illuminate\Contracts\Session\Session
  624. */
  625. public function getSession()
  626. {
  627. return $this->session;
  628. }
  629. /**
  630. * Return the currently cached user.
  631. *
  632. * @return \Illuminate\Contracts\Auth\Authenticatable|null
  633. */
  634. public function getUser()
  635. {
  636. return $this->user;
  637. }
  638. /**
  639. * Set the current user.
  640. *
  641. * @param \Illuminate\Contracts\Auth\Authenticatable $user
  642. * @return $this
  643. */
  644. public function setUser(AuthenticatableContract $user)
  645. {
  646. $this->user = $user;
  647. $this->loggedOut = false;
  648. $this->fireAuthenticatedEvent($user);
  649. return $this;
  650. }
  651. /**
  652. * Get the current request instance.
  653. *
  654. * @return \Symfony\Component\HttpFoundation\Request
  655. */
  656. public function getRequest()
  657. {
  658. return $this->request ?: Request::createFromGlobals();
  659. }
  660. /**
  661. * Set the current request instance.
  662. *
  663. * @param \Symfony\Component\HttpFoundation\Request $request
  664. * @return $this
  665. */
  666. public function setRequest(Request $request)
  667. {
  668. $this->request = $request;
  669. return $this;
  670. }
  671. }