AbstractProvider.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. <?php
  2. /*
  3. * This file is part of the overtrue/socialite.
  4. *
  5. * (c) overtrue <i@overtrue.me>
  6. *
  7. * This source file is subject to the MIT license that is bundled
  8. * with this source code in the file LICENSE.
  9. */
  10. namespace Overtrue\Socialite\Providers;
  11. use GuzzleHttp\Client;
  12. use GuzzleHttp\ClientInterface;
  13. use Overtrue\Socialite\AccessToken;
  14. use Overtrue\Socialite\AccessTokenInterface;
  15. use Overtrue\Socialite\AuthorizeFailedException;
  16. use Overtrue\Socialite\InvalidStateException;
  17. use Overtrue\Socialite\ProviderInterface;
  18. use Symfony\Component\HttpFoundation\RedirectResponse;
  19. use Symfony\Component\HttpFoundation\Request;
  20. /**
  21. * Class AbstractProvider.
  22. */
  23. abstract class AbstractProvider implements ProviderInterface
  24. {
  25. /**
  26. * Provider name.
  27. *
  28. * @var string
  29. */
  30. protected $name;
  31. /**
  32. * The HTTP request instance.
  33. *
  34. * @var \Symfony\Component\HttpFoundation\Request
  35. */
  36. protected $request;
  37. /**
  38. * The client ID.
  39. *
  40. * @var string
  41. */
  42. protected $clientId;
  43. /**
  44. * The client secret.
  45. *
  46. * @var string
  47. */
  48. protected $clientSecret;
  49. /**
  50. * @var \Overtrue\Socialite\AccessTokenInterface
  51. */
  52. protected $accessToken;
  53. /**
  54. * The redirect URL.
  55. *
  56. * @var string
  57. */
  58. protected $redirectUrl;
  59. /**
  60. * The custom parameters to be sent with the request.
  61. *
  62. * @var array
  63. */
  64. protected $parameters = [];
  65. /**
  66. * The scopes being requested.
  67. *
  68. * @var array
  69. */
  70. protected $scopes = [];
  71. /**
  72. * The separating character for the requested scopes.
  73. *
  74. * @var string
  75. */
  76. protected $scopeSeparator = ',';
  77. /**
  78. * The type of the encoding in the query.
  79. *
  80. * @var int Can be either PHP_QUERY_RFC3986 or PHP_QUERY_RFC1738
  81. */
  82. protected $encodingType = PHP_QUERY_RFC1738;
  83. /**
  84. * Indicates if the session state should be utilized.
  85. *
  86. * @var bool
  87. */
  88. protected $stateless = false;
  89. /**
  90. * Create a new provider instance.
  91. *
  92. * @param \Symfony\Component\HttpFoundation\Request $request
  93. * @param string $clientId
  94. * @param string $clientSecret
  95. * @param string|null $redirectUrl
  96. */
  97. public function __construct(Request $request, $clientId, $clientSecret, $redirectUrl = null)
  98. {
  99. $this->request = $request;
  100. $this->clientId = $clientId;
  101. $this->clientSecret = $clientSecret;
  102. $this->redirectUrl = $redirectUrl;
  103. }
  104. /**
  105. * Get the authentication URL for the provider.
  106. *
  107. * @param string $state
  108. *
  109. * @return string
  110. */
  111. abstract protected function getAuthUrl($state);
  112. /**
  113. * Get the token URL for the provider.
  114. *
  115. * @return string
  116. */
  117. abstract protected function getTokenUrl();
  118. /**
  119. * Get the raw user for the given access token.
  120. *
  121. * @param \Overtrue\Socialite\AccessTokenInterface $token
  122. *
  123. * @return array
  124. */
  125. abstract protected function getUserByToken(AccessTokenInterface $token);
  126. /**
  127. * Map the raw user array to a Socialite User instance.
  128. *
  129. * @param array $user
  130. *
  131. * @return \Overtrue\Socialite\User
  132. */
  133. abstract protected function mapUserToObject(array $user);
  134. /**
  135. * Redirect the user of the application to the provider's authentication screen.
  136. *
  137. * @param string $redirectUrl
  138. *
  139. * @return \Symfony\Component\HttpFoundation\RedirectResponse
  140. */
  141. public function redirect($redirectUrl = null)
  142. {
  143. $state = null;
  144. if (!is_null($redirectUrl)) {
  145. $this->redirectUrl = $redirectUrl;
  146. }
  147. if ($this->usesState()) {
  148. $state = $this->makeState();
  149. }
  150. return new RedirectResponse($this->getAuthUrl($state));
  151. }
  152. /**
  153. * {@inheritdoc}
  154. */
  155. public function user(AccessTokenInterface $token = null)
  156. {
  157. if (is_null($token) && $this->hasInvalidState()) {
  158. throw new InvalidStateException();
  159. }
  160. $token = $token ?: $this->getAccessToken($this->getCode());
  161. $user = $this->getUserByToken($token);
  162. $user = $this->mapUserToObject($user)->merge(['original' => $user]);
  163. return $user->setToken($token)->setProviderName($this->getName());
  164. }
  165. /**
  166. * Set redirect url.
  167. *
  168. * @param string $redirectUrl
  169. *
  170. * @return $this
  171. */
  172. public function setRedirectUrl($redirectUrl)
  173. {
  174. $this->redirectUrl = $redirectUrl;
  175. return $this;
  176. }
  177. /**
  178. * Set redirect url.
  179. *
  180. * @param string $redirectUrl
  181. *
  182. * @return $this
  183. */
  184. public function withRedirectUrl($redirectUrl)
  185. {
  186. $this->redirectUrl = $redirectUrl;
  187. return $this;
  188. }
  189. /**
  190. * Return the redirect url.
  191. *
  192. * @return string
  193. */
  194. public function getRedirectUrl()
  195. {
  196. return $this->redirectUrl;
  197. }
  198. /**
  199. * @param \Overtrue\Socialite\AccessTokenInterface $accessToken
  200. *
  201. * @return $this
  202. */
  203. public function setAccessToken(AccessTokenInterface $accessToken)
  204. {
  205. $this->accessToken = $accessToken;
  206. return $this;
  207. }
  208. /**
  209. * Get the access token for the given code.
  210. *
  211. * @param string $code
  212. *
  213. * @return \Overtrue\Socialite\AccessTokenInterface
  214. */
  215. public function getAccessToken($code)
  216. {
  217. if ($this->accessToken) {
  218. return $this->accessToken;
  219. }
  220. $postKey = (version_compare(ClientInterface::VERSION, '6') === 1) ? 'form_params' : 'body';
  221. $response = $this->getHttpClient()->post($this->getTokenUrl(), [
  222. 'headers' => ['Accept' => 'application/json'],
  223. $postKey => $this->getTokenFields($code),
  224. ]);
  225. return $this->parseAccessToken($response->getBody());
  226. }
  227. /**
  228. * Set the scopes of the requested access.
  229. *
  230. * @param array $scopes
  231. *
  232. * @return $this
  233. */
  234. public function scopes(array $scopes)
  235. {
  236. $this->scopes = $scopes;
  237. return $this;
  238. }
  239. /**
  240. * Set the request instance.
  241. *
  242. * @param Request $request
  243. *
  244. * @return $this
  245. */
  246. public function setRequest(Request $request)
  247. {
  248. $this->request = $request;
  249. return $this;
  250. }
  251. /**
  252. * Get the request instance.
  253. *
  254. * @return \Symfony\Component\HttpFoundation\Request
  255. */
  256. public function getRequest()
  257. {
  258. return $this->request;
  259. }
  260. /**
  261. * Indicates that the provider should operate as stateless.
  262. *
  263. * @return $this
  264. */
  265. public function stateless()
  266. {
  267. $this->stateless = true;
  268. return $this;
  269. }
  270. /**
  271. * Set the custom parameters of the request.
  272. *
  273. * @param array $parameters
  274. *
  275. * @return $this
  276. */
  277. public function with(array $parameters)
  278. {
  279. $this->parameters = $parameters;
  280. return $this;
  281. }
  282. /**
  283. * @return string
  284. */
  285. public function getName()
  286. {
  287. if (empty($this->name)) {
  288. $this->name = strstr((new \ReflectionClass(get_class($this)))->getShortName(), 'Provider', true);
  289. }
  290. return $this->name;
  291. }
  292. /**
  293. * Get the authentication URL for the provider.
  294. *
  295. * @param string $url
  296. * @param string $state
  297. *
  298. * @return string
  299. */
  300. protected function buildAuthUrlFromBase($url, $state)
  301. {
  302. return $url.'?'.http_build_query($this->getCodeFields($state), '', '&', $this->encodingType);
  303. }
  304. /**
  305. * Get the GET parameters for the code request.
  306. *
  307. * @param string|null $state
  308. *
  309. * @return array
  310. */
  311. protected function getCodeFields($state = null)
  312. {
  313. $fields = array_merge([
  314. 'client_id' => $this->clientId,
  315. 'redirect_uri' => $this->redirectUrl,
  316. 'scope' => $this->formatScopes($this->scopes, $this->scopeSeparator),
  317. 'response_type' => 'code',
  318. ], $this->parameters);
  319. if ($this->usesState()) {
  320. $fields['state'] = $state;
  321. }
  322. return $fields;
  323. }
  324. /**
  325. * Format the given scopes.
  326. *
  327. * @param array $scopes
  328. * @param string $scopeSeparator
  329. *
  330. * @return string
  331. */
  332. protected function formatScopes(array $scopes, $scopeSeparator)
  333. {
  334. return implode($scopeSeparator, $scopes);
  335. }
  336. /**
  337. * Determine if the current request / session has a mismatching "state".
  338. *
  339. * @return bool
  340. */
  341. protected function hasInvalidState()
  342. {
  343. if ($this->isStateless()) {
  344. return false;
  345. }
  346. $state = $this->request->getSession()->get('state');
  347. return !(strlen($state) > 0 && $this->request->get('state') === $state);
  348. }
  349. /**
  350. * Get the POST fields for the token request.
  351. *
  352. * @param string $code
  353. *
  354. * @return array
  355. */
  356. protected function getTokenFields($code)
  357. {
  358. return [
  359. 'client_id' => $this->clientId,
  360. 'client_secret' => $this->clientSecret,
  361. 'code' => $code,
  362. 'redirect_uri' => $this->redirectUrl,
  363. ];
  364. }
  365. /**
  366. * Get the access token from the token response body.
  367. *
  368. * @param \Psr\Http\Message\StreamInterface|array $body
  369. *
  370. * @return \Overtrue\Socialite\AccessTokenInterface
  371. */
  372. protected function parseAccessToken($body)
  373. {
  374. if (!is_array($body)) {
  375. $body = json_decode($body, true);
  376. }
  377. if (empty($body['access_token'])) {
  378. throw new AuthorizeFailedException('Authorize Failed: '.json_encode($body, JSON_UNESCAPED_UNICODE), $body);
  379. }
  380. return new AccessToken($body);
  381. }
  382. /**
  383. * Get the code from the request.
  384. *
  385. * @return string
  386. */
  387. protected function getCode()
  388. {
  389. return $this->request->get('code');
  390. }
  391. /**
  392. * Get a fresh instance of the Guzzle HTTP client.
  393. *
  394. * @return \GuzzleHttp\Client
  395. */
  396. protected function getHttpClient()
  397. {
  398. return new Client(['http_errors' => false]);
  399. }
  400. /**
  401. * Determine if the provider is operating with state.
  402. *
  403. * @return bool
  404. */
  405. protected function usesState()
  406. {
  407. return !$this->stateless;
  408. }
  409. /**
  410. * Determine if the provider is operating as stateless.
  411. *
  412. * @return bool
  413. */
  414. protected function isStateless()
  415. {
  416. return $this->stateless;
  417. }
  418. /**
  419. * Return array item by key.
  420. *
  421. * @param array $array
  422. * @param string $key
  423. * @param mixed $default
  424. *
  425. * @return mixed
  426. */
  427. protected function arrayItem(array $array, $key, $default = null)
  428. {
  429. if (is_null($key)) {
  430. return $array;
  431. }
  432. if (isset($array[$key])) {
  433. return $array[$key];
  434. }
  435. foreach (explode('.', $key) as $segment) {
  436. if (!is_array($array) || !array_key_exists($segment, $array)) {
  437. return $default;
  438. }
  439. $array = $array[$segment];
  440. }
  441. return $array;
  442. }
  443. /**
  444. * Put state to session storage and return it.
  445. *
  446. * @return string|bool
  447. */
  448. protected function makeState()
  449. {
  450. $state = sha1(uniqid(mt_rand(1, 1000000), true));
  451. $session = $this->request->getSession();
  452. if (is_callable([$session, 'put'])) {
  453. $session->put('state', $state);
  454. } elseif (is_callable([$session, 'set'])) {
  455. $session->set('state', $state);
  456. } else {
  457. return false;
  458. }
  459. return $state;
  460. }
  461. }