/src/Appwrite/Auth/OAuth2/Apple.php

https://github.com/appwrite/appwrite · PHP · 233 lines · 146 code · 40 blank · 47 comment · 13 complexity · f2754d920ab6775be729f6e99a8a537b MD5 · raw file

  1. <?php
  2. namespace Appwrite\Auth\OAuth2;
  3. use Appwrite\Auth\OAuth2;
  4. use Exception;
  5. // Reference Material
  6. // https://developer.okta.com/blog/2019/06/04/what-the-heck-is-sign-in-with-apple
  7. class Apple extends OAuth2
  8. {
  9. /**
  10. * @var array
  11. */
  12. protected $user = [];
  13. /**
  14. * @var array
  15. */
  16. protected $scopes = [
  17. "name",
  18. "email"
  19. ];
  20. /**
  21. * @var array
  22. */
  23. protected $claims = [];
  24. /**
  25. * @return string
  26. */
  27. public function getName(): string
  28. {
  29. return 'apple';
  30. }
  31. /**
  32. * @return string
  33. */
  34. public function getLoginURL(): string
  35. {
  36. return 'https://appleid.apple.com/auth/authorize?'.\http_build_query([
  37. 'client_id' => $this->appID,
  38. 'redirect_uri' => $this->callback,
  39. 'state' => \json_encode($this->state),
  40. 'response_type' => 'code',
  41. 'response_mode' => 'form_post',
  42. 'scope' => \implode(' ', $this->getScopes())
  43. ]);
  44. }
  45. /**
  46. * @param string $code
  47. *
  48. * @return string
  49. */
  50. public function getAccessToken(string $code): string
  51. {
  52. $headers[] = 'Content-Type: application/x-www-form-urlencoded';
  53. $accessToken = $this->request(
  54. 'POST',
  55. 'https://appleid.apple.com/auth/token',
  56. $headers,
  57. \http_build_query([
  58. 'grant_type' => 'authorization_code',
  59. 'code' => $code,
  60. 'client_id' => $this->appID,
  61. 'client_secret' => $this->getAppSecret(),
  62. 'redirect_uri' => $this->callback,
  63. ])
  64. );
  65. $accessToken = \json_decode($accessToken, true);
  66. $this->claims = (isset($accessToken['id_token'])) ? \explode('.', $accessToken['id_token']) : [0 => '', 1 => ''];
  67. $this->claims = (isset($this->claims[1])) ? \json_decode(\base64_decode($this->claims[1]), true) : [];
  68. if (isset($accessToken['access_token'])) {
  69. return $accessToken['access_token'];
  70. }
  71. return '';
  72. }
  73. /**
  74. * @param string $accessToken
  75. *
  76. * @return string
  77. */
  78. public function getUserID(string $accessToken): string
  79. {
  80. if (isset($this->claims['sub']) && !empty($this->claims['sub'])) {
  81. return $this->claims['sub'];
  82. }
  83. return '';
  84. }
  85. /**
  86. * @param string $accessToken
  87. *
  88. * @return string
  89. */
  90. public function getUserEmail(string $accessToken): string
  91. {
  92. if (isset($this->claims['email']) &&
  93. !empty($this->claims['email']) &&
  94. isset($this->claims['email_verified']) &&
  95. $this->claims['email_verified'] === 'true') {
  96. return $this->claims['email'];
  97. }
  98. return '';
  99. }
  100. /**
  101. * @param string $accessToken
  102. *
  103. * @return string
  104. */
  105. public function getUserName(string $accessToken): string
  106. {
  107. if (isset($this->claims['email']) &&
  108. !empty($this->claims['email']) &&
  109. isset($this->claims['email_verified']) &&
  110. $this->claims['email_verified'] === 'true') {
  111. return $this->claims['email'];
  112. }
  113. return '';
  114. }
  115. protected function getAppSecret():string
  116. {
  117. try {
  118. $secret = \json_decode($this->appSecret, true);
  119. } catch (\Throwable $th) {
  120. throw new Exception('Invalid secret');
  121. }
  122. $keyfile = (isset($secret['p8'])) ? $secret['p8'] : ''; // Your p8 Key file
  123. $keyID = (isset($secret['keyID'])) ? $secret['keyID'] : ''; // Your Key ID
  124. $teamID = (isset($secret['teamID'])) ? $secret['teamID'] : ''; // Your Team ID (see Developer Portal)
  125. $bundleID = $this->appID; // Your Bundle ID
  126. $headers = [
  127. 'alg' => 'ES256',
  128. 'kid' => $keyID,
  129. ];
  130. $claims = [
  131. 'iss' => $teamID,
  132. 'iat' => \time(),
  133. 'exp' => \time() + 86400*180,
  134. 'aud' => 'https://appleid.apple.com',
  135. 'sub' => $bundleID,
  136. ];
  137. $pkey = \openssl_pkey_get_private($keyfile);
  138. $payload = $this->encode(\json_encode($headers)).'.'.$this->encode(\json_encode($claims));
  139. $signature = '';
  140. $success = \openssl_sign($payload, $signature, $pkey, OPENSSL_ALGO_SHA256);
  141. if (!$success) {
  142. return '';
  143. }
  144. return $payload.'.'.$this->encode($this->fromDER($signature, 64));
  145. }
  146. /**
  147. * @param string $data
  148. */
  149. protected function encode($data)
  150. {
  151. return \str_replace(['+', '/', '='], ['-', '_', ''], \base64_encode($data));
  152. }
  153. /**
  154. * @param string $data
  155. */
  156. protected function retrievePositiveInteger(string $data): string
  157. {
  158. while ('00' === \mb_substr($data, 0, 2, '8bit') && \mb_substr($data, 2, 2, '8bit') > '7f') {
  159. $data = \mb_substr($data, 2, null, '8bit');
  160. }
  161. return $data;
  162. }
  163. /**
  164. * @param string $der
  165. * @param int $partLength
  166. */
  167. protected function fromDER(string $der, int $partLength):string
  168. {
  169. $hex = \unpack('H*', $der)[1];
  170. if ('30' !== \mb_substr($hex, 0, 2, '8bit')) { // SEQUENCE
  171. throw new \RuntimeException();
  172. }
  173. if ('81' === \mb_substr($hex, 2, 2, '8bit')) { // LENGTH > 128
  174. $hex = \mb_substr($hex, 6, null, '8bit');
  175. } else {
  176. $hex = \mb_substr($hex, 4, null, '8bit');
  177. }
  178. if ('02' !== \mb_substr($hex, 0, 2, '8bit')) { // INTEGER
  179. throw new \RuntimeException();
  180. }
  181. $Rl = \hexdec(\mb_substr($hex, 2, 2, '8bit'));
  182. $R = $this->retrievePositiveInteger(\mb_substr($hex, 4, $Rl * 2, '8bit'));
  183. $R = \str_pad($R, $partLength, '0', STR_PAD_LEFT);
  184. $hex = \mb_substr($hex, 4 + $Rl * 2, null, '8bit');
  185. if ('02' !== \mb_substr($hex, 0, 2, '8bit')) { // INTEGER
  186. throw new \RuntimeException();
  187. }
  188. $Sl = \hexdec(\mb_substr($hex, 2, 2, '8bit'));
  189. $S = $this->retrievePositiveInteger(\mb_substr($hex, 4, $Sl * 2, '8bit'));
  190. $S = \str_pad($S, $partLength, '0', STR_PAD_LEFT);
  191. return \pack('H*', $R.$S);
  192. }
  193. }