diff --git a/.config/.env_example b/.config/.env_example index 5ab2ebb..204dd39 100644 --- a/.config/.env_example +++ b/.config/.env_example @@ -230,6 +230,12 @@ STRIPE_SECRET= STRIPE_CALLBACK_URL= #STRIPE_TESTUSER= +# https://developers.tiktok.com/apps/ +TIKTOK_KEY= +TIKTOK_SECRET= +TIKTOK_CALLBACK_URL= +#TIKTOK_TESTUSER= + # https://www.tumblr.com/oauth/apps TUMBLR_KEY= TUMBLR_SECRET= diff --git a/examples/get-token/TikTok.php b/examples/get-token/TikTok.php new file mode 100644 index 0000000..cc989cb --- /dev/null +++ b/examples/get-token/TikTok.php @@ -0,0 +1,21 @@ + + * @copyright 2024 smiley + * @license MIT + */ +declare(strict_types=1); + +use chillerlan\OAuth\Providers\TikTok; + +require_once __DIR__.'/../provider-example-common.php'; + +/** @var \OAuthExampleProviderFactory $factory */ +$provider = $factory->getProvider(TikTok::class); + +require_once __DIR__.'/_flow-oauth2.php'; + +exit; diff --git a/src/Providers/TikTok.php b/src/Providers/TikTok.php new file mode 100644 index 0000000..e400e54 --- /dev/null +++ b/src/Providers/TikTok.php @@ -0,0 +1,103 @@ + + * @copyright 2024 smiley + * @license MIT + * + * @noinspection PhpUnused + */ + +namespace chillerlan\OAuth\Providers; + +use chillerlan\OAuth\Core\{CSRFToken, OAuth2Provider, PKCE, TokenRefresh}; +use function array_merge, implode; + +/** + * @see https://developers.tiktok.com/doc/login-kit-web/ + * @see https://developers.tiktok.com/doc/oauth-user-access-token-management/ + */ +class TikTok extends OAuth2Provider implements CSRFToken, PKCE, TokenRefresh{ + + public const IDENTIFIER = 'TIKTOK'; + + public const SCOPE_VIDEO_UPLOAD = 'video.upload'; + public const SCOPE_VIDEO_LIST = 'video.list'; + public const SCOPE_VIDEO_PUBLISH = 'video.publish'; + public const SCOPE_USER_INFO_BASIC = 'user.info.basic'; + public const SCOPE_USER_INFO_PROFILE = 'user.info.profile'; + public const SCOPE_USER_INFO_STATS = 'user.info.stats'; + public const SCOPE_PORTABILITY_PPOSTPROFILE_ONGOING = 'portability.postsandprofile.ongoing'; + public const SCOPE_PORTABILITY_PPOSTPROFILE_SINGLE = 'portability.postsandprofile.single'; + public const SCOPE_PORTABILITY_ALL_ONGOING = 'portability.all.ongoing'; + public const SCOPE_PORTABILITY_ALL_SINGLE = 'portability.all.single'; + public const SCOPE_PORTABILITY_DIRECTMESSAGES_ONGOING = 'portability.directmessages.ongoing'; + public const SCOPE_PORTABILITY_DIRECTMESSAGES_SINGLE = 'portability.directmessages.single'; + public const SCOPE_PORTABILITY_ACTIVITY_ONGOING = 'portability.activity.ongoing'; + public const SCOPE_PORTABILITY_ACTIVITY_SINGLE = 'portability.activity.single'; + + public const DEFAULT_SCOPES = [ + self::SCOPE_USER_INFO_BASIC, + ]; + + protected string $authorizationURL = 'https://www.tiktok.com/v2/auth/authorize/'; + protected string $accessTokenURL = 'https://open.tiktokapis.com/v2/oauth/token/'; + protected string $revokeURL = 'https://open.tiktokapis.com/v2/oauth/revoke/'; + protected string $apiURL = 'https://open.tiktokapis.com'; + protected string|null $apiDocs = 'https://developers.tiktok.com/doc/overview/'; + protected string|null $applicationURL = 'https://developers.tiktok.com/apps/'; + protected string|null $userRevokeURL = 'https://example.com/user/settings/connections'; + + /** + * @inheritDoc + */ + protected function getAuthorizationURLRequestParams(array $params, array $scopes):array{ + + unset($params['client_secret']); + + $params = array_merge($params, [ + 'client_key' => $this->options->key, + 'redirect_uri' => $this->options->callbackURL, + 'response_type' => 'code', + ]); + + if(!empty($scopes)){ + $params['scope'] = implode($this::SCOPES_DELIMITER, $scopes); + } + + $params = $this->setCodeChallenge($params, PKCE::CHALLENGE_METHOD_S256); + + return $this->setState($params); + } + + /** + * @inheritDoc + */ + protected function getAccessTokenRequestBodyParams(string $code):array{ + + $params = [ + 'client_key' => $this->options->key, + 'client_secret' => $this->options->secret, + 'code' => $code, + 'grant_type' => 'authorization_code', + 'redirect_uri' => $this->options->callbackURL, + ]; + + return $this->setCodeVerifier($params); + } + + /** + * @inheritDoc + */ + protected function getRefreshAccessTokenRequestBodyParams(string $refreshToken):array{ + return [ + 'client_key' => $this->options->key, + 'client_secret' => $this->options->secret, + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken, + ]; + } + +} diff --git a/tests/Providers/Unit/TikTokTest.php b/tests/Providers/Unit/TikTokTest.php new file mode 100644 index 0000000..d587127 --- /dev/null +++ b/tests/Providers/Unit/TikTokTest.php @@ -0,0 +1,80 @@ + + * @copyright 2024 smiley + * @license MIT + */ +declare(strict_types=1); + +namespace chillerlan\OAuthTest\Providers\Unit; + +use chillerlan\HTTP\Utils\QueryUtil; +use chillerlan\OAuth\Providers\TikTok; +use function implode; + +/** + * @property \chillerlan\OAuth\Providers\TikTok $provider + */ +final class TikTokTest extends OAuth2ProviderUnitTestAbstract{ + + protected function getProviderFQCN():string{ + return TikTok::class; + } + + public function testGetAuthURL():void{ + $uri = $this->provider->getAuthorizationURL(); + $params = QueryUtil::parse($uri->getQuery()); + + $this::assertSame($this->getReflectionProperty('authorizationURL'), (string)$uri->withQuery('')); + + $this::assertArrayHasKey('client_key', $params); + $this::assertArrayHasKey('redirect_uri', $params); + $this::assertArrayHasKey('response_type', $params); + + $this::assertArrayHasKey('state', $params); + } + + public function testGetAuthURLRequestParams():void{ + $extraparams = ['response_type' => 'whatever', 'foo' => 'bar']; + $scopes = ['scope1', 'scope2', 'scope3']; + + $params = $this->invokeReflectionMethod('getAuthorizationURLRequestParams', [$extraparams, $scopes]); + + $this::assertSame($this->options->key, $params['client_key']); + $this::assertSame($this->options->callbackURL, $params['redirect_uri']); + $this::assertSame('code', $params['response_type']); + $this::assertSame(implode($this->provider::SCOPES_DELIMITER, $scopes), $params['scope']); + $this::assertSame('bar', $params['foo']); + } + + public function testGetAccessTokenRequestBodyParams():void{ + $verifier = $this->provider->generateVerifier($this->options->pkceVerifierLength); + + $this->storage->storeCodeVerifier($verifier, $this->provider->name); + + $params = $this->invokeReflectionMethod('getAccessTokenRequestBodyParams', ['*test_code*']); + + $this::assertSame('*test_code*', $params['code']); + $this::assertSame($this->options->callbackURL, $params['redirect_uri']); + $this::assertSame('authorization_code', $params['grant_type']); + $this::assertSame($this->options->key, $params['client_key']); + $this::assertSame($this->options->secret, $params['client_secret']); + + $this::assertSame($verifier, $params['code_verifier']); + + } + + public function testGetRefreshAccessTokenRequestBodyParams():void{ + $params = $this->invokeReflectionMethod('getRefreshAccessTokenRequestBodyParams', ['*refresh_token*']); + + $this::assertSame('*refresh_token*', $params['refresh_token']); + $this::assertSame($this->options->key, $params['client_key']); + $this::assertSame($this->options->secret, $params['client_secret']); + $this::assertSame('refresh_token', $params['grant_type']); + } + + +}