Add Kick OAuth adapter

This commit is contained in:
Matej Bačo
2026-04-27 14:02:30 +02:00
parent 2e960b90df
commit 15f94d99ca
7 changed files with 334 additions and 0 deletions
+11
View File
@@ -200,6 +200,17 @@ return [
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Google',
],
'kick' => [
'name' => 'Kick',
'developers' => 'https://docs.kick.com/',
'icon' => 'icon-kick',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Kick',
],
'linkedin' => [
'name' => 'LinkedIn',
'developers' => 'https://developer.linkedin.com/',
+2
View File
@@ -123,6 +123,7 @@ use Appwrite\Utopia\Response\Model\OAuth2Figma;
use Appwrite\Utopia\Response\Model\OAuth2GitHub;
use Appwrite\Utopia\Response\Model\OAuth2Gitlab;
use Appwrite\Utopia\Response\Model\OAuth2Google;
use Appwrite\Utopia\Response\Model\OAuth2Kick;
use Appwrite\Utopia\Response\Model\OAuth2Linkedin;
use Appwrite\Utopia\Response\Model\OAuth2Notion;
use Appwrite\Utopia\Response\Model\OAuth2Oidc;
@@ -421,6 +422,7 @@ Response::setModel(new OAuth2Authentik());
Response::setModel(new OAuth2Auth0());
Response::setModel(new OAuth2Oidc());
Response::setModel(new OAuth2Okta());
Response::setModel(new OAuth2Kick());
Response::setModel(new OAuth2Apple());
Response::setModel(new PolicyPasswordDictionary());
Response::setModel(new PolicyPasswordHistory());
+230
View File
@@ -0,0 +1,230 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
// Reference Material
// https://docs.kick.com/getting-started/generating-tokens-oauth2-flow
// https://docs.kick.com/getting-started/scopes
class Kick extends OAuth2
{
private const PKCE_STATE_KEY = '_pkce';
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @var array
*/
protected array $scopes = [
'user:read',
];
/**
* @var string
*/
private string $pkceVerifier = '';
/**
* @return string
*/
public function getName(): string
{
return 'kick';
}
/**
* @return string
*/
public function getLoginURL(): string
{
$state = $this->state;
$state[self::PKCE_STATE_KEY] = $this->getPKCEVerifier();
return 'https://id.kick.com/oauth/authorize?' . \http_build_query([
'response_type' => 'code',
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'state' => \json_encode($state),
'code_challenge' => $this->getPKCEChallenge(),
'code_challenge_method' => 'S256',
]);
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
'https://id.kick.com/oauth/token',
$headers,
\http_build_query([
'grant_type' => 'authorization_code',
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'code_verifier' => $this->getPKCEVerifier(),
'code' => $code,
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
'https://id.kick.com/oauth/token',
$headers,
\http_build_query([
'grant_type' => 'refresh_token',
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'refresh_token' => $refreshToken,
])
), true);
if (empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserID(string $accessToken): string
{
$user = $this->getUser($accessToken);
return isset($user['user_id']) ? (string)$user['user_id'] : '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['email'] ?? '';
}
/**
* Check if the OAuth email is verified.
*
* Kick only returns an email when the user has granted the `user:read`
* scope and the account email is verified, so a non-empty email is
* treated as verified.
*
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
return !empty($this->getUserEmail($accessToken));
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['name'] ?? '';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken): array
{
if (empty($this->user)) {
$headers = ['Authorization: Bearer ' . $accessToken];
$response = \json_decode($this->request(
'GET',
'https://api.kick.com/public/v1/users',
$headers
), true);
$this->user = $response['data'][0] ?? [];
}
return $this->user;
}
/**
* Extract the PKCE verifier from the state on the callback so the same
* value generated in getLoginURL() can be sent to the token endpoint.
*
* @param string $state
*
* @return array<string, mixed>|null
*/
public function parseState(string $state): ?array
{
$parsed = \json_decode($state, true);
if (!\is_array($parsed)) {
return null;
}
$verifier = $parsed[self::PKCE_STATE_KEY] ?? null;
if (\is_string($verifier)) {
$this->pkceVerifier = $verifier;
}
unset($parsed[self::PKCE_STATE_KEY]);
return $parsed;
}
private function getPKCEVerifier(): string
{
if ($this->pkceVerifier === '') {
$this->pkceVerifier = \rtrim(\strtr(\base64_encode(\random_bytes(64)), '+/', '-_'), '=');
}
return $this->pkceVerifier;
}
private function getPKCEChallenge(): string
{
return \rtrim(\strtr(\base64_encode(\hash('sha256', $this->getPKCEVerifier(), true)), '+/', '-_'), '=');
}
}
@@ -0,0 +1,45 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Kick;
use Appwrite\Auth\OAuth2\Kick;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'kick';
}
public static function getProviderClass(): string
{
return Kick::class;
}
public static function getProviderLabel(): string
{
return 'Kick';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Kick';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_KICK;
}
public static function getClientIdDescription(): string
{
return '\'Client ID\' of Kick OAuth2 app. For example: 01KQ7C00000000000001MFHS32';
}
public static function getClientSecretDescription(): string
{
return '\'Client Secret\' of Kick OAuth2 app. For example: 34ac5600000000000000000000000000000000000000000000000000e830c8b';
}
}
@@ -33,6 +33,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Figma\Update as Update
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\GitHub\Update as UpdateOAuth2GitHub;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Gitlab\Update as UpdateOAuth2Gitlab;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Google\Update as UpdateOAuth2Google;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Kick\Update as UpdateOAuth2Kick;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Linkedin\Update as UpdateOAuth2Linkedin;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Notion\Update as UpdateOAuth2Notion;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Oidc\Update as UpdateOAuth2Oidc;
@@ -205,5 +206,6 @@ class Http extends Service
$this->addAction(UpdateOAuth2Auth0::getName(), new UpdateOAuth2Auth0());
$this->addAction(UpdateOAuth2Oidc::getName(), new UpdateOAuth2Oidc());
$this->addAction(UpdateOAuth2Okta::getName(), new UpdateOAuth2Okta());
$this->addAction(UpdateOAuth2Kick::getName(), new UpdateOAuth2Kick());
}
}
+1
View File
@@ -314,6 +314,7 @@ class Response extends SwooleResponse
public const MODEL_OAUTH2_OIDC = 'oAuth2Oidc';
public const MODEL_OAUTH2_APPLE = 'oAuth2Apple';
public const MODEL_OAUTH2_OKTA = 'oAuth2Okta';
public const MODEL_OAUTH2_KICK = 'oAuth2Kick';
// Health
public const MODEL_HEALTH_STATUS = 'healthStatus';
@@ -0,0 +1,43 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Kick extends OAuth2Base
{
public function getProviderLabel(): string
{
return 'Kick';
}
public function getClientIdExample(): string
{
return '01KQ7C00000000000001MFHS32';
}
public function getClientSecretExample(): string
{
return '34ac5600000000000000000000000000000000000000000000000000e830c8b';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Kick';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_KICK;
}
}