Skip to content

Commit 85d9b19

Browse files
authored
:octocat: +TikTok provider (#1)
1 parent 5331e2a commit 85d9b19

File tree

7 files changed

+256
-0
lines changed

7 files changed

+256
-0
lines changed

.config/.env_example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,12 @@ TIDAL_SECRET=
236236
TIDAL_CALLBACK_URL=
237237
#TIDAL_TESTUSER=
238238

239+
# https://developers.tiktok.com/apps/
240+
TIKTOK_KEY=
241+
TIKTOK_SECRET=
242+
TIKTOK_CALLBACK_URL=
243+
#TIKTOK_TESTUSER=
244+
239245
# https://www.tumblr.com/oauth/apps
240246
TUMBLR_KEY=
241247
TUMBLR_SECRET=

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ Note: check the [releases](https://github.com/chillerlan/php-oauth/releases) for
144144
| [Steam](https://developer.valvesoftware.com/wiki/Steam_Web_API) | [link](https://steamcommunity.com/dev/apikey) | | - || | | | | |
145145
| [Stripe](https://stripe.com/docs/api) | [link](https://dashboard.stripe.com/apikeys) | [link](https://dashboard.stripe.com/account/applications) | 2 ||| | |||
146146
| [Tidal](https://developer.tidal.com/documentation) | [link](https://developer.tidal.com/dashboard) | [link](https://account.tidal.com/third-party-apps) | 2 |||||| |
147+
| [TikTok](https://developers.tiktok.com/doc/overview/) | [link](https://developers.tiktok.com/apps/) | [link](https://example.com/user/settings/connections) | 2 | ||| || |
147148
| [Tumblr](https://www.tumblr.com/docs/en/api/v2) | [link](https://www.tumblr.com/oauth/apps) | [link](https://www.tumblr.com/settings/apps) | 1 || | | | | |
148149
| [Tumblr2](https://www.tumblr.com/docs/en/api/v2) | [link](https://www.tumblr.com/oauth/apps) | [link](https://www.tumblr.com/settings/apps) | 2 ||| ||| |
149150
| [Twitch](https://dev.twitch.tv/docs/api/reference/) | [link](https://dev.twitch.tv/console/apps/create) | [link](https://www.twitch.tv/settings/connections) | 2 ||| ||||

docs/Basics/Overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ fully [PSR-7](https://www.php-fig.org/psr/psr-7/)/[PSR-17](https://www.php-fig.o
7979
| [Steam](https://developer.valvesoftware.com/wiki/Steam_Web_API) | [link](https://steamcommunity.com/dev/apikey) | | - || | | | | |
8080
| [Stripe](https://stripe.com/docs/api) | [link](https://dashboard.stripe.com/apikeys) | [link](https://dashboard.stripe.com/account/applications) | 2 ||| | |||
8181
| [Tidal](https://developer.tidal.com/documentation) | [link](https://developer.tidal.com/dashboard) | [link](https://account.tidal.com/third-party-apps) | 2 |||||| |
82+
| [TikTok](https://developers.tiktok.com/doc/overview/) | [link](https://developers.tiktok.com/apps/) | [link](https://example.com/user/settings/connections) | 2 | ||| || |
8283
| [Tumblr](https://www.tumblr.com/docs/en/api/v2) | [link](https://www.tumblr.com/oauth/apps) | [link](https://www.tumblr.com/settings/apps) | 1 || | | | | |
8384
| [Tumblr2](https://www.tumblr.com/docs/en/api/v2) | [link](https://www.tumblr.com/oauth/apps) | [link](https://www.tumblr.com/settings/apps) | 2 ||| ||| |
8485
| [Twitch](https://dev.twitch.tv/docs/api/reference/) | [link](https://dev.twitch.tv/console/apps/create) | [link](https://www.twitch.tv/settings/connections) | 2 ||| ||||

examples/get-token/TikTok.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
/**
3+
* @link https://developers.tiktok.com/doc/login-kit-web/
4+
*
5+
* @created 11.04.2024
6+
* @author Smiley <[email protected]>
7+
* @copyright 2024 smiley
8+
* @license MIT
9+
*/
10+
declare(strict_types=1);
11+
12+
use chillerlan\OAuth\Providers\TikTok;
13+
14+
require_once __DIR__.'/../provider-example-common.php';
15+
16+
/** @var \OAuthExampleProviderFactory $factory */
17+
$provider = $factory->getProvider(TikTok::class);
18+
19+
require_once __DIR__.'/_flow-oauth2.php';
20+
21+
exit;

src/Providers/TikTok.php

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
/**
3+
* Class TikTok
4+
*
5+
* @created 11.04.2024
6+
* @author smiley <[email protected]>
7+
* @copyright 2024 smiley
8+
* @license MIT
9+
*
10+
* @noinspection PhpUnused
11+
*/
12+
declare(strict_types=1);
13+
14+
namespace chillerlan\OAuth\Providers;
15+
16+
use chillerlan\OAuth\Core\{AuthenticatedUser, CSRFToken, OAuth2Provider, PKCE, PKCETrait, TokenRefresh, UserInfo};
17+
use function array_merge, implode;
18+
19+
/**
20+
* @see https://developers.tiktok.com/doc/login-kit-web/
21+
* @see https://developers.tiktok.com/doc/oauth-user-access-token-management/
22+
*/
23+
class TikTok extends OAuth2Provider implements CSRFToken, PKCE, TokenRefresh, UserInfo{
24+
use PKCETrait;
25+
26+
public const IDENTIFIER = 'TIKTOK';
27+
28+
public const SCOPES_DELIMITER = ',';
29+
30+
public const SCOPE_VIDEO_UPLOAD = 'video.upload';
31+
public const SCOPE_VIDEO_LIST = 'video.list';
32+
public const SCOPE_VIDEO_PUBLISH = 'video.publish';
33+
public const SCOPE_USER_INFO_BASIC = 'user.info.basic';
34+
public const SCOPE_USER_INFO_PROFILE = 'user.info.profile';
35+
public const SCOPE_USER_INFO_STATS = 'user.info.stats';
36+
public const SCOPE_PORTABILITY_PPOSTPROFILE_ONGOING = 'portability.postsandprofile.ongoing';
37+
public const SCOPE_PORTABILITY_PPOSTPROFILE_SINGLE = 'portability.postsandprofile.single';
38+
public const SCOPE_PORTABILITY_ALL_ONGOING = 'portability.all.ongoing';
39+
public const SCOPE_PORTABILITY_ALL_SINGLE = 'portability.all.single';
40+
public const SCOPE_PORTABILITY_DIRECTMESSAGES_ONGOING = 'portability.directmessages.ongoing';
41+
public const SCOPE_PORTABILITY_DIRECTMESSAGES_SINGLE = 'portability.directmessages.single';
42+
public const SCOPE_PORTABILITY_ACTIVITY_ONGOING = 'portability.activity.ongoing';
43+
public const SCOPE_PORTABILITY_ACTIVITY_SINGLE = 'portability.activity.single';
44+
45+
public const DEFAULT_SCOPES = [
46+
self::SCOPE_USER_INFO_BASIC,
47+
self::SCOPE_USER_INFO_PROFILE,
48+
self::SCOPE_USER_INFO_STATS,
49+
self::SCOPE_VIDEO_LIST,
50+
];
51+
52+
protected string $authorizationURL = 'https://www.tiktok.com/v2/auth/authorize/';
53+
protected string $accessTokenURL = 'https://open.tiktokapis.com/v2/oauth/token/';
54+
protected string $revokeURL = 'https://open.tiktokapis.com/v2/oauth/revoke/';
55+
protected string $apiURL = 'https://open.tiktokapis.com';
56+
protected string|null $apiDocs = 'https://developers.tiktok.com/doc/overview/';
57+
protected string|null $applicationURL = 'https://developers.tiktok.com/apps/';
58+
protected string|null $userRevokeURL = 'https://example.com/user/settings/connections';
59+
60+
/**
61+
* @inheritDoc
62+
*/
63+
protected function getAuthorizationURLRequestParams(array $params, array $scopes):array{
64+
65+
unset($params['client_secret']);
66+
67+
$params = array_merge($params, [
68+
'client_key' => $this->options->key,
69+
'redirect_uri' => $this->options->callbackURL,
70+
'response_type' => 'code',
71+
]);
72+
73+
if(!empty($scopes)){
74+
$params['scope'] = implode($this::SCOPES_DELIMITER, $scopes);
75+
}
76+
77+
$params = $this->setCodeChallenge($params, PKCE::CHALLENGE_METHOD_S256);
78+
79+
return $this->setState($params);
80+
}
81+
82+
/**
83+
* @inheritDoc
84+
*/
85+
protected function getAccessTokenRequestBodyParams(string $code):array{
86+
87+
$params = [
88+
'client_key' => $this->options->key,
89+
'client_secret' => $this->options->secret,
90+
'code' => $code,
91+
'grant_type' => 'authorization_code',
92+
'redirect_uri' => $this->options->callbackURL,
93+
];
94+
95+
return $this->setCodeVerifier($params);
96+
}
97+
98+
/**
99+
* @inheritDoc
100+
*/
101+
protected function getRefreshAccessTokenRequestBodyParams(string $refreshToken):array{
102+
return [
103+
'client_key' => $this->options->key,
104+
'client_secret' => $this->options->secret,
105+
'grant_type' => 'refresh_token',
106+
'refresh_token' => $refreshToken,
107+
];
108+
}
109+
110+
public function me():AuthenticatedUser{
111+
$params = ['fields' => 'open_id,avatar_url,display_name,profile_deep_link,username,is_verified'];
112+
$json = $this->getMeResponseData('/v2/user/info/', $params);
113+
114+
$userdata = [
115+
'avatar' => $json['data']['user']['avatar_url'],
116+
'data' => $json,
117+
'displayName' => $json['data']['user']['display_name'],
118+
'handle' => $json['data']['user']['username'],
119+
'id' => $json['data']['user']['open_id'],
120+
'url' => $json['data']['user']['profile_deep_link'],
121+
];
122+
123+
return new AuthenticatedUser($userdata);
124+
}
125+
126+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
/**
3+
* Class TikTokAPITest
4+
*
5+
* @created 06.04.2018
6+
* @author Smiley <[email protected]>
7+
* @copyright 2018 Smiley
8+
* @license MIT
9+
*/
10+
declare(strict_types=1);
11+
12+
use chillerlan\OAuth\Providers\TikTok;
13+
use chillerlan\OAuthTest\Attributes\Provider;
14+
use chillerlan\OAuthTest\Providers\Live\OAuth2ProviderLiveTestAbstract;
15+
use PHPUnit\Framework\Attributes\Group;
16+
17+
/**
18+
* @property \chillerlan\OAuth\Providers\TikTok $provider
19+
*/
20+
#[Group('providerLiveTest')]
21+
#[Provider(TikTok::class)]
22+
final class TikTokAPITest extends OAuth2ProviderLiveTestAbstract{
23+
24+
}

tests/Providers/Unit/TikTokTest.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
/**
3+
* Class TikTokTest
4+
*
5+
* @created 18.04.2024
6+
* @author smiley <[email protected]>
7+
* @copyright 2024 smiley
8+
* @license MIT
9+
*/
10+
declare(strict_types=1);
11+
12+
namespace chillerlan\OAuthTest\Providers\Unit;
13+
14+
use chillerlan\HTTP\Utils\QueryUtil;
15+
use chillerlan\OAuth\Providers\TikTok;
16+
use chillerlan\OAuthTest\Attributes\Provider;
17+
use function implode;
18+
19+
/**
20+
* @property \chillerlan\OAuth\Providers\TikTok $provider
21+
*/
22+
#[Provider(TikTok::class)]
23+
final class TikTokTest extends OAuth2ProviderUnitTestAbstract{
24+
25+
public function testGetAuthURL():void{
26+
$uri = $this->provider->getAuthorizationURL();
27+
$params = QueryUtil::parse($uri->getQuery());
28+
29+
$this::assertSame($this->getReflectionProperty('authorizationURL'), (string)$uri->withQuery(''));
30+
31+
$this::assertArrayHasKey('client_key', $params);
32+
$this::assertArrayHasKey('redirect_uri', $params);
33+
$this::assertArrayHasKey('response_type', $params);
34+
35+
$this::assertArrayHasKey('state', $params);
36+
}
37+
38+
public function testGetAuthURLRequestParams():void{
39+
$extraparams = ['response_type' => 'whatever', 'foo' => 'bar'];
40+
$scopes = ['scope1', 'scope2', 'scope3'];
41+
42+
$params = $this->invokeReflectionMethod('getAuthorizationURLRequestParams', [$extraparams, $scopes]);
43+
44+
$this::assertSame($this->options->key, $params['client_key']);
45+
$this::assertSame($this->options->callbackURL, $params['redirect_uri']);
46+
$this::assertSame('code', $params['response_type']);
47+
$this::assertSame(implode($this->provider::SCOPES_DELIMITER, $scopes), $params['scope']);
48+
$this::assertSame('bar', $params['foo']);
49+
}
50+
51+
public function testGetAccessTokenRequestBodyParams():void{
52+
$verifier = $this->provider->generateVerifier($this->options->pkceVerifierLength);
53+
54+
$this->storage->storeCodeVerifier($verifier, $this->provider->getName());
55+
56+
$params = $this->invokeReflectionMethod('getAccessTokenRequestBodyParams', ['*test_code*']);
57+
58+
$this::assertSame('*test_code*', $params['code']);
59+
$this::assertSame($this->options->callbackURL, $params['redirect_uri']);
60+
$this::assertSame('authorization_code', $params['grant_type']);
61+
$this::assertSame($this->options->key, $params['client_key']);
62+
$this::assertSame($this->options->secret, $params['client_secret']);
63+
64+
$this::assertSame($verifier, $params['code_verifier']);
65+
66+
}
67+
68+
public function testGetRefreshAccessTokenRequestBodyParams():void{
69+
$params = $this->invokeReflectionMethod('getRefreshAccessTokenRequestBodyParams', ['*refresh_token*']);
70+
71+
$this::assertSame('*refresh_token*', $params['refresh_token']);
72+
$this::assertSame($this->options->key, $params['client_key']);
73+
$this::assertSame($this->options->secret, $params['client_secret']);
74+
$this::assertSame('refresh_token', $params['grant_type']);
75+
}
76+
77+
}

0 commit comments

Comments
 (0)