FeiShu.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. <?php
  2. namespace Overtrue\Socialite\Providers;
  3. use GuzzleHttp\Exception\GuzzleException;
  4. use Overtrue\Socialite\Exceptions\AuthorizeFailedException;
  5. use Overtrue\Socialite\Exceptions\BadRequestException;
  6. use Overtrue\Socialite\Exceptions\Feishu\InvalidTicketException;
  7. use Overtrue\Socialite\Exceptions\InvalidTokenException;
  8. use Overtrue\Socialite\User;
  9. /**
  10. * @see https://open.feishu.cn/document/uQjL04CN/ucDOz4yN4MjL3gzM
  11. */
  12. class FeiShu extends Base
  13. {
  14. public const NAME = 'feishu';
  15. protected string $baseUrl = 'https://open.feishu.cn/open-apis';
  16. protected string $expiresInKey = 'refresh_expires_in';
  17. protected bool $isInternalApp = false;
  18. public function __construct(array $config)
  19. {
  20. parent::__construct($config);
  21. $this->isInternalApp = ($this->config->get('app_mode') ?? $this->config->get('mode')) == 'internal';
  22. }
  23. protected function getAuthUrl(): string
  24. {
  25. return $this->buildAuthUrlFromBase($this->baseUrl . '/authen/v1/index');
  26. }
  27. protected function getCodeFields(): array
  28. {
  29. return [
  30. 'redirect_uri' => $this->redirectUrl,
  31. 'app_id' => $this->getClientId(),
  32. ];
  33. }
  34. protected function getTokenUrl(): string
  35. {
  36. return $this->baseUrl . '/authen/v1/access_token';
  37. }
  38. /**
  39. * @param string $code
  40. *
  41. * @return array
  42. * @throws AuthorizeFailedException
  43. * @throws GuzzleException
  44. */
  45. public function tokenFromCode(string $code): array
  46. {
  47. return $this->normalizeAccessTokenResponse($this->getTokenFromCode($code));
  48. }
  49. /**
  50. * @param string $code
  51. *
  52. * @return array
  53. * @throws AuthorizeFailedException
  54. *
  55. * @throws AuthorizeFailedException
  56. * @throws GuzzleException
  57. */
  58. protected function getTokenFromCode(string $code): array
  59. {
  60. $this->configAppAccessToken();
  61. $response = $this->getHttpClient()->post(
  62. $this->getTokenUrl(),
  63. [
  64. 'json' => [
  65. 'app_access_token' => $this->config->get('app_access_token'),
  66. 'code' => $code,
  67. 'grant_type' => 'authorization_code',
  68. ],
  69. ]
  70. );
  71. $response = \json_decode($response->getBody(), true) ?? [];
  72. if (empty($response['data'])) {
  73. throw new AuthorizeFailedException('Invalid token response', $response);
  74. }
  75. return $this->normalizeAccessTokenResponse($response['data']);
  76. }
  77. /**
  78. * @param string $token
  79. *
  80. * @return array
  81. * @throws GuzzleException
  82. */
  83. protected function getUserByToken(string $token): array
  84. {
  85. $response = $this->getHttpClient()->get(
  86. $this->baseUrl . '/authen/v1/user_info',
  87. [
  88. 'headers' => ['Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $token],
  89. 'query' => array_filter(
  90. [
  91. 'user_access_token' => $token,
  92. ]
  93. ),
  94. ]
  95. );
  96. $response = \json_decode($response->getBody(), true) ?? [];
  97. if (empty($response['data'])) {
  98. throw new \InvalidArgumentException('You have error! ' . json_encode($response, JSON_UNESCAPED_UNICODE));
  99. }
  100. return $response['data'];
  101. }
  102. /**
  103. * @param array $user
  104. *
  105. * @return User
  106. */
  107. protected function mapUserToObject(array $user): User
  108. {
  109. return new User(
  110. [
  111. 'id' => $user['user_id'] ?? null,
  112. 'name' => $user['name'] ?? null,
  113. 'nickname' => $user['name'] ?? null,
  114. 'avatar' => $user['avatar_url'] ?? null,
  115. 'email' => $user['email'] ?? null,
  116. ]
  117. );
  118. }
  119. public function withInternalAppMode(): self
  120. {
  121. $this->isInternalApp = true;
  122. return $this;
  123. }
  124. public function withDefaultMode(): self
  125. {
  126. $this->isInternalApp = false;
  127. return $this;
  128. }
  129. /**
  130. * set 'app_ticket' in config attribute
  131. *
  132. * @param string $appTicket
  133. *
  134. * @return FeiShu
  135. */
  136. public function withAppTicket(string $appTicket): self
  137. {
  138. $this->config->set('app_ticket', $appTicket);
  139. return $this;
  140. }
  141. /**
  142. * 设置 app_access_token 到 config 设置中
  143. * 应用维度授权凭证,开放平台可据此识别调用方的应用身份
  144. * 分内建和自建
  145. */
  146. protected function configAppAccessToken()
  147. {
  148. $url = $this->baseUrl . '/auth/v3/app_access_token/';
  149. $params = [
  150. 'json' => [
  151. 'app_id' => $this->config->get('client_id'),
  152. 'app_secret' => $this->config->get('client_secret'),
  153. 'app_ticket' => $this->config->get('app_ticket'),
  154. ],
  155. ];
  156. if ($this->isInternalApp) {
  157. $url = $this->baseUrl . '/auth/v3/app_access_token/internal/';
  158. $params = [
  159. 'json' => [
  160. 'app_id' => $this->config->get('client_id'),
  161. 'app_secret' => $this->config->get('client_secret'),
  162. ],
  163. ];
  164. }
  165. if (!$this->isInternalApp && !$this->config->has('app_ticket')) {
  166. throw new InvalidTicketException('You are using default mode, please config \'app_ticket\' first');
  167. }
  168. $response = $this->getHttpClient()->post($url, $params);
  169. $response = \json_decode($response->getBody(), true) ?? [];
  170. if (empty($response['app_access_token'])) {
  171. throw new InvalidTokenException('Invalid \'app_access_token\' response', json_encode($response));
  172. }
  173. $this->config->set('app_access_token', $response['app_access_token']);
  174. }
  175. /**
  176. * 设置 tenant_access_token 到 config 属性中
  177. * 应用的企业授权凭证,开放平台据此识别调用方的应用身份和企业身份
  178. * 分内建和自建
  179. */
  180. protected function configTenantAccessToken()
  181. {
  182. $url = $this->baseUrl . '/auth/v3/tenant_access_token/';
  183. $params = [
  184. 'json' => [
  185. 'app_id' => $this->config->get('client_id'),
  186. 'app_secret' => $this->config->get('client_secret'),
  187. 'app_ticket' => $this->config->get('app_ticket'),
  188. ],
  189. ];
  190. if ($this->isInternalApp) {
  191. $url = $this->baseUrl . '/auth/v3/tenant_access_token/internal/';
  192. $params = [
  193. 'json' => [
  194. 'app_id' => $this->config->get('client_id'),
  195. 'app_secret' => $this->config->get('client_secret'),
  196. ],
  197. ];
  198. }
  199. if (!$this->isInternalApp && !$this->config->has('app_ticket')) {
  200. throw new BadRequestException('You are using default mode, please config \'app_ticket\' first');
  201. }
  202. $response = $this->getHttpClient()->post($url, $params);
  203. $response = \json_decode($response->getBody(), true) ?? [];
  204. if (empty($response['tenant_access_token'])) {
  205. throw new AuthorizeFailedException('Invalid tenant_access_token response', $response);
  206. }
  207. $this->config->set('tenant_access_token', $response['tenant_access_token']);
  208. }
  209. }