friendship ended with social-app. php is my new best friend
1<?php 2/** 3 * Class Twitch 4 * 5 * @created 22.10.2017 6 * @author Smiley <smiley@chillerlan.net> 7 * @copyright 2017 Smiley 8 * @license MIT 9 * 10 * @noinspection PhpUnused 11 */ 12declare(strict_types=1); 13 14namespace chillerlan\OAuth\Providers; 15 16use chillerlan\HTTP\Utils\QueryUtil; 17use chillerlan\OAuth\Core\{ 18 AccessToken, AuthenticatedUser, ClientCredentials, ClientCredentialsTrait, CSRFToken, InvalidAccessTokenException, 19 OAuth2Provider, TokenInvalidate, TokenInvalidateTrait, TokenRefresh, UserInfo, 20}; 21use Psr\Http\Message\{RequestInterface, ResponseInterface}; 22use function implode, sprintf; 23use const PHP_QUERY_RFC1738; 24 25/** 26 * Twitch OAuth2 27 * 28 * @link https://dev.twitch.tv/docs/api/reference/ 29 * @link https://dev.twitch.tv/docs/authentication/ 30 * @link https://dev.twitch.tv/docs/authentication#oauth-client-credentials-flow-app-access-tokens 31 */ 32class Twitch extends OAuth2Provider implements ClientCredentials, CSRFToken, TokenInvalidate, TokenRefresh, UserInfo{ 33 use ClientCredentialsTrait, TokenInvalidateTrait; 34 35 public const IDENTIFIER = 'TWITCH'; 36 37 public const SCOPE_ANALYTICS_READ_EXTENSIONS = 'analytics:read:extensions'; 38 public const SCOPE_ANALYTICS_READ_GAMES = 'analytics:read:games'; 39 public const SCOPE_BITS_READ = 'bits:read'; 40 public const SCOPE_CHANNEL_EDIT_COMMERCIAL = 'channel:edit:commercial'; 41 public const SCOPE_CHANNEL_MANAGE_BROADCAST = 'channel:manage:broadcast'; 42 public const SCOPE_CHANNEL_MANAGE_EXTENSIONS = 'channel:manage:extensions'; 43 public const SCOPE_CHANNEL_MANAGE_REDEMPTIONS = 'channel:manage:redemptions'; 44 public const SCOPE_CHANNEL_MANAGE_VIDEOS = 'channel:manage:videos'; 45 public const SCOPE_CHANNEL_READ_EDITORS = 'channel:read:editors'; 46 public const SCOPE_CHANNEL_READ_HYPE_TRAIN = 'channel:read:hype_train'; 47 public const SCOPE_CHANNEL_READ_REDEMPTIONS = 'channel:read:redemptions'; 48 public const SCOPE_CHANNEL_READ_STREAM_KEY = 'channel:read:stream_key'; 49 public const SCOPE_CHANNEL_READ_SUBSCRIPTIONS = 'channel:read:subscriptions'; 50 public const SCOPE_CLIPS_EDIT = 'clips:edit'; 51 public const SCOPE_MODERATION_READ = 'moderation:read'; 52 public const SCOPE_USER_EDIT = 'user:edit'; 53 public const SCOPE_USER_EDIT_FOLLOWS = 'user:edit:follows'; 54 public const SCOPE_USER_READ_BLOCKED_USERS = 'user:read:blocked_users'; 55 public const SCOPE_USER_MANAGE_BLOCKED_USERS = 'user:manage:blocked_users'; 56 public const SCOPE_USER_READ_BROADCAST = 'user:read:broadcast'; 57 public const SCOPE_USER_READ_EMAIL = 'user:read:email'; 58 public const SCOPE_USER_READ_SUBSCRIPTIONS = 'user:read:subscriptions'; 59 60 public const DEFAULT_SCOPES = [ 61 self::SCOPE_USER_READ_EMAIL, 62 ]; 63 64 public const HEADERS_AUTH = [ 65 'Accept' => 'application/vnd.twitchtv.v5+json', 66 ]; 67 68 public const HEADERS_API = [ 69 'Accept' => 'application/vnd.twitchtv.v5+json', 70 ]; 71 72 protected string $authorizationURL = 'https://id.twitch.tv/oauth2/authorize'; 73 protected string $accessTokenURL = 'https://id.twitch.tv/oauth2/token'; 74 protected string $revokeURL = 'https://id.twitch.tv/oauth2/revoke'; 75 protected string $apiURL = 'https://api.twitch.tv'; 76 protected string|null $userRevokeURL = 'https://www.twitch.tv/settings/connections'; 77 protected string|null $apiDocs = 'https://dev.twitch.tv/docs/api/reference/'; 78 protected string|null $applicationURL = 'https://dev.twitch.tv/console/apps/create'; 79 80 /** 81 * @param string[]|null $scopes 82 * @return array<string, string> 83 */ 84 protected function getClientCredentialsTokenRequestBodyParams(array|null $scopes):array{ 85 86 $params = [ 87 'client_id' => $this->options->key, 88 'client_secret' => $this->options->secret, 89 'grant_type' => 'client_credentials', 90 ]; 91 92 if($scopes !== null){ 93 $params['scope'] = implode($this::SCOPES_DELIMITER, $scopes); 94 } 95 96 return $params; 97 } 98 99 /** 100 * @param array<string, string> $body 101 */ 102 protected function sendClientCredentialsTokenRequest(string $url, array $body):ResponseInterface{ 103 104 $request = $this->requestFactory 105 ->createRequest('POST', $url) 106 ->withHeader('Accept-Encoding', 'identity') 107 ->withHeader('Content-Type', 'application/x-www-form-urlencoded') 108 ->withBody($this->streamFactory->createStream(QueryUtil::build($body, PHP_QUERY_RFC1738))) 109 ; 110 111 foreach($this::HEADERS_AUTH as $header => $value){ 112 $request = $request->withAddedHeader($header, $value); 113 } 114 115 return $this->http->sendRequest($request); 116 } 117 118 /** 119 * @return array<string, scalar|bool|null> 120 */ 121 protected function getInvalidateAccessTokenBodyParams(AccessToken $token, string $type):array{ 122 return [ 123 'client_id' => $this->options->key, 124 'token' => $token->accessToken, 125 'token_type_hint' => $type, 126 ]; 127 } 128 129 public function getRequestAuthorization(RequestInterface $request, AccessToken|null $token = null):RequestInterface{ 130 $token ??= $this->storage->getAccessToken($this->name); 131 132 if($token->isExpired()){ 133 134 if($this->options->tokenAutoRefresh !== true){ 135 throw new InvalidAccessTokenException; 136 } 137 138 $token = $this->refreshAccessToken($token); 139 } 140 141 return $request 142 ->withHeader('Authorization', $this::AUTH_PREFIX_HEADER.' '.$token->accessToken) 143 ->withHeader('Client-ID', $this->options->key); 144 } 145 146 /** @codeCoverageIgnore */ 147 public function me():AuthenticatedUser{ 148 $json = $this->getMeResponseData('/helix/users'); 149 $user = $json['data'][0]; 150 151 $userdata = [ 152 'data' => $user, 153 'avatar' => $user['profile_image_url'], 154 'handle' => $user['login'], 155 'displayName' => $user['display_name'], 156 'email' => $user['email'], 157 'id' => $user['id'], 158 'url' => sprintf('https://www.twitch.tv/%s', $user['login']), 159 ]; 160 161 return new AuthenticatedUser($userdata); 162 } 163 164}