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}