abstract oauth adapters

This commit is contained in:
Matej Bačo
2026-04-24 12:06:58 +02:00
parent 335b1c2f6c
commit dac184b281
7 changed files with 221 additions and 373 deletions
-7
View File
@@ -50,13 +50,6 @@ abstract class OAuth2
$this->addScope($scope);
}
}
/**
* Check if the OAuth credentials are valid
*
* @throws \Exception
*/
abstract public function verifyCredentials(): void;
/**
* @return string
-4
View File
@@ -184,8 +184,4 @@ class Discord extends OAuth2
return $this->user;
}
public function verifyCredentials(): void {
// TODO: Implement, eventuelly. Refer to GitHub.php in this directory for inspiration
}
}
-4
View File
@@ -175,8 +175,4 @@ class Figma extends OAuth2
return $this->user;
}
public function verifyCredentials(): void {
// TODO: Implement, eventuelly. Refer to GitHub.php in this directory for inspiration
}
}
@@ -0,0 +1,175 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
abstract class Base extends Action
{
use HTTP;
/**
* Provider ID used in paths, database keys and event labels.
*
* @return string e.g. 'github', 'discord', 'figma'
*/
abstract public static function getProviderId(): string;
/**
* Provider OAuth2 implementation class. Must implement verifyCredentials().
*
* @return class-string e.g. Github::class
*/
abstract public static function getProviderClass(): string;
/**
* Provider display label used in descriptions, SDK method name and action name.
*
* @return string e.g. 'GitHub', 'Discord', 'Figma'
*/
abstract public static function getProviderLabel(): string;
/**
* Response model constant for this provider.
*
* @return string e.g. Response::MODEL_OAUTH2_GITHUB
*/
abstract public static function getResponseModel(): string;
/**
* Description of the clientId param, including an example value.
*
* @return string
*/
abstract public static function getClientIdDescription(): string;
/**
* Description of the clientSecret param, including an example value.
*
* @return string
*/
abstract public static function getClientSecretDescription(): string;
public static function getName()
{
return 'updateProjectOAuth2' . static::getProviderLabel();
}
public function __construct()
{
$providerId = static::getProviderId();
$providerLabel = static::getProviderLabel();
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/oauth2/' . $providerId)
->desc('Update project OAuth2 ' . $providerLabel)
->groups(['api', 'project'])
->label('scope', 'oauth2.write')
->label('event', 'oauth2.' . $providerId . '.update')
->label('audits.event', 'project.oauth2.' . $providerId . '.update')
->label('audits.resource', 'project.oauth2/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'oauth2',
name: 'updateOAuth2' . $providerLabel,
description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.',
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: static::getResponseModel(),
)
],
))
->param('clientId', null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true)
->param('clientSecret', null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->action(...));
}
public function action(
?string $clientId,
?string $clientSecret,
?bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
): void {
$providerId = static::getProviderId();
if(!(\in_array($providerId, \array_keys(Config::getParam('oAuthProviders'))))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Provider ' . $providerId . ' is not supported by server configuration.');
}
$oAuthProviders = $project->getAttribute('oAuthProviders', []);
$appIdKey = $providerId . 'Appid';
$appSecretKey = $providerId . 'Secret';
$enabledKey = $providerId . 'Enabled';
if (!\is_null($clientId)) {
$oAuthProviders[$appIdKey] = $clientId;
}
if (!\is_null($clientSecret)) {
$oAuthProviders[$appSecretKey] = $clientSecret;
}
if (!\is_null($enabled)) {
$oAuthProviders[$enabledKey] = $enabled;
}
if($enabled === true || \is_null($enabled)) {
try {
if(empty($oAuthProviders[$appIdKey]) || empty($oAuthProviders[$appSecretKey])) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Client ID and Client Secret are required when enabling OAuth2 provider.');
}
$providerClass = static::getProviderClass();
$providerInstance = new $providerClass(appId: $oAuthProviders[$appIdKey], appSecret: $oAuthProviders[$appSecretKey], callback: '', state: [], scopes: []);
// E2E integration check
if(\method_exists($providerInstance,'verifyCredentials')) {
$providerInstance->verifyCredentials();
}
$oAuthProviders[$enabledKey] = true;
} catch(\Throwable $err) {
if($enabled === true) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Could not enable OAuth2 provider: ' . $err->getMessage());
}
}
}
$updates = new Document([
'oAuthProviders' => $oAuthProviders
]);
$project = $authorization->skip(fn() => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
$response->dynamic(new Document([
'$id' => $providerId,
'enabled' => $oAuthProviders[$enabledKey] ?? false,
'clientId' => $oAuthProviders[$appIdKey] ?? '',
'clientSecret' => $oAuthProviders[$appSecretKey] ?? '',
]), static::getResponseModel());
}
}
@@ -3,142 +3,38 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Discord;
use Appwrite\Auth\OAuth2\Discord;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Action
class Update extends Base
{
use HTTP;
public static function getName()
{
return 'updateProjectOAuth2Discord';
}
public static function getProviderId(): string
{
return 'discord';
}
/**
* @return class-string
*/
public static function getProviderClass(): string
{
return Discord::class;
}
public function __construct()
public static function getProviderLabel(): string
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/oauth2/discord')
->desc('Update project OAuth2 Discord')
->groups(['api', 'project'])
->label('scope', 'oauth2.write')
->label('event', 'oauth2.discord.update')
->label('audits.event', 'project.oauth2.discord.update')
->label('audits.resource', 'project.oauth2/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'oauth2',
name: 'updateOAuth2Discord',
description: <<<EOT
Update the project OAuth2 Discord configuration.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_OAUTH2_DISCORD,
)
],
))
->param('clientId', null, new Nullable(new Text(256, 0)), 'Client ID of Discord OAuth2 app. For example: 950722000000343754', optional: true)
->param('clientSecret', null, new Nullable(new Text(512, 0)), 'Client Secret of Discord OAuth2 app. For example: YmPXnM000000000000000000002zFg5D', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->action(...));
return 'Discord';
}
public function action(
?string $clientId,
?string $clientSecret,
?bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
): void {
$providerId = self::getProviderId();
if(!(\in_array($providerId, \array_keys(Config::getParam('oAuthProviders'))))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Provider ' . $providerId . ' is not supported by server configuration.');
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_DISCORD;
}
$oAuthProviders = $project->getAttribute('oAuthProviders', []);
public static function getClientIdDescription(): string
{
return 'Client ID of Discord OAuth2 app. For example: 950722000000343754';
}
$appIdKey = $providerId . 'Appid';
$appSecretKey = $providerId . 'Secret';
$enabledKey = $providerId . 'Enabled';
if (!\is_null($clientId)) {
$oAuthProviders[$appIdKey] = $clientId;
}
if (!\is_null($clientSecret)) {
$oAuthProviders[$appSecretKey] = $clientSecret;
}
if (!\is_null($enabled)) {
$oAuthProviders[$enabledKey] = $enabled;
}
if($enabled === true || \is_null($enabled)) {
try {
if(empty($oAuthProviders[$appIdKey]) || empty($oAuthProviders[$appSecretKey])) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Client ID and Client Secret are required when enabling OAuth2 provider.');
}
$providerClass = self::getProviderClass();
$providerInstance = new $providerClass(appId: $oAuthProviders[$appIdKey], appSecret: $oAuthProviders[$appSecretKey], callback: '', state: [], scopes: []);
$providerInstance->verifyCredentials();
$oAuthProviders[$enabledKey] = true;
} catch(\Throwable $err) {
if($enabled === true) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Could not enable OAuth2 provider: ' . $err->getMessage());
}
}
}
$updates = new Document([
'oAuthProviders' => $oAuthProviders
]);
$project = $authorization->skip(fn() => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
$response->dynamic(new Document([
'$id' => $providerId,
'enabled' => $oAuthProviders[$enabledKey] ?? false,
'clientId' => $oAuthProviders[$appIdKey] ?? '',
'clientSecret' => $oAuthProviders[$appSecretKey] ?? '',
]), Response::MODEL_OAUTH2_DISCORD);
public static function getClientSecretDescription(): string
{
return 'Client Secret of Discord OAuth2 app. For example: YmPXnM000000000000000000002zFg5D';
}
}
@@ -3,142 +3,38 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Figma;
use Appwrite\Auth\OAuth2\Figma;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Action
class Update extends Base
{
use HTTP;
public static function getName()
{
return 'updateProjectOAuth2Figma';
}
public static function getProviderId(): string
{
return 'figma';
}
/**
* @return class-string
*/
public static function getProviderClass(): string
{
return Figma::class;
}
public function __construct()
public static function getProviderLabel(): string
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/oauth2/figma')
->desc('Update project OAuth2 Figma')
->groups(['api', 'project'])
->label('scope', 'oauth2.write')
->label('event', 'oauth2.figma.update')
->label('audits.event', 'project.oauth2.figma.update')
->label('audits.resource', 'project.oauth2/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'oauth2',
name: 'updateOAuth2Figma',
description: <<<EOT
Update the project OAuth2 Figma configuration.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_OAUTH2_FIGMA,
)
],
))
->param('clientId', null, new Nullable(new Text(256, 0)), 'Client ID of Figma OAuth2 app. For example: byay5H0000000000VtiI40', optional: true)
->param('clientSecret', null, new Nullable(new Text(512, 0)), 'Client Secret of Figma OAuth2 app. For example: yEpOYn0000000000000000004iIsU5', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->action(...));
return 'Figma';
}
public function action(
?string $clientId,
?string $clientSecret,
?bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
): void {
$providerId = self::getProviderId();
if(!(\in_array($providerId, \array_keys(Config::getParam('oAuthProviders'))))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Provider ' . $providerId . ' is not supported by server configuration.');
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_FIGMA;
}
$oAuthProviders = $project->getAttribute('oAuthProviders', []);
public static function getClientIdDescription(): string
{
return 'Client ID of Figma OAuth2 app. For example: byay5H0000000000VtiI40';
}
$appIdKey = $providerId . 'Appid';
$appSecretKey = $providerId . 'Secret';
$enabledKey = $providerId . 'Enabled';
if (!\is_null($clientId)) {
$oAuthProviders[$appIdKey] = $clientId;
}
if (!\is_null($clientSecret)) {
$oAuthProviders[$appSecretKey] = $clientSecret;
}
if (!\is_null($enabled)) {
$oAuthProviders[$enabledKey] = $enabled;
}
if($enabled === true || \is_null($enabled)) {
try {
if(empty($oAuthProviders[$appIdKey]) || empty($oAuthProviders[$appSecretKey])) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Client ID and Client Secret are required when enabling OAuth2 provider.');
}
$providerClass = self::getProviderClass();
$providerInstance = new $providerClass(appId: $oAuthProviders[$appIdKey], appSecret: $oAuthProviders[$appSecretKey], callback: '', state: [], scopes: []);
$providerInstance->verifyCredentials();
$oAuthProviders[$enabledKey] = true;
} catch(\Throwable $err) {
if($enabled === true) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Could not enable OAuth2 provider: ' . $err->getMessage());
}
}
}
$updates = new Document([
'oAuthProviders' => $oAuthProviders
]);
$project = $authorization->skip(fn() => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
$response->dynamic(new Document([
'$id' => $providerId,
'enabled' => $oAuthProviders[$enabledKey] ?? false,
'clientId' => $oAuthProviders[$appIdKey] ?? '',
'clientSecret' => $oAuthProviders[$appSecretKey] ?? '',
]), Response::MODEL_OAUTH2_FIGMA);
public static function getClientSecretDescription(): string
{
return 'Client Secret of Figma OAuth2 app. For example: yEpOYn0000000000000000004iIsU5';
}
}
@@ -3,142 +3,38 @@
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\GitHub;
use Appwrite\Auth\OAuth2\Github;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Action
class Update extends Base
{
use HTTP;
public static function getName()
{
return 'updateProjectOAuth2GitHub';
}
public static function getProviderId(): string
{
return 'github';
}
/**
* @return class-string
*/
public static function getProviderClass(): string
{
return Github::class;
}
public function __construct()
public static function getProviderLabel(): string
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/oauth2/github')
->desc('Update project OAuth2 GitHub')
->groups(['api', 'project'])
->label('scope', 'oauth2.write')
->label('event', 'oauth2.github.update')
->label('audits.event', 'project.oauth2.github.update')
->label('audits.resource', 'project.oauth2/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'oauth2',
name: 'updateOAuth2GitHub',
description: <<<EOT
Update the project OAuth2 GitHub configuration.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_OAUTH2_GITHUB,
)
],
))
->param('clientId', null, new Nullable(new Text(256, 0)), 'Client ID of GitHub OAuth2 app, or App ID of GitHub generic app. For example: e4d87900000000540733', optional: true)
->param('clientSecret', null, new Nullable(new Text(512, 0)), 'Client secret of GitHub OAuth2 app, or GitHub generic app. For example: 5e07c00000000000000000000000000000198bcc', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->action(...));
return 'GitHub';
}
public function action(
?string $clientId,
?string $clientSecret,
?bool $enabled,
Response $response,
Database $dbForPlatform,
Document $project,
Authorization $authorization
): void {
$providerId = self::getProviderId();
if(!(\in_array($providerId, \array_keys(Config::getParam('oAuthProviders'))))) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Provider ' . $providerId . ' is not supported by server configuration.');
}
$oAuthProviders = $project->getAttribute('oAuthProviders', []);
$appIdKey = $providerId . 'Appid';
$appSecretKey = $providerId . 'Secret';
$enabledKey = $providerId . 'Enabled';
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_GITHUB;
}
if (!\is_null($clientId)) {
$oAuthProviders[$appIdKey] = $clientId;
}
if (!\is_null($clientSecret)) {
$oAuthProviders[$appSecretKey] = $clientSecret;
}
public static function getClientIdDescription(): string
{
return 'Client ID of GitHub OAuth2 app, or App ID of GitHub generic app. For example: e4d87900000000540733';
}
if (!\is_null($enabled)) {
$oAuthProviders[$enabledKey] = $enabled;
}
if($enabled === true || \is_null($enabled)) {
try {
if(empty($oAuthProviders[$appIdKey]) || empty($oAuthProviders[$appSecretKey])) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Client ID and Client Secret are required when enabling OAuth2 provider.');
}
$providerClass = self::getProviderClass();
$providerInstance = new $providerClass(appId: $oAuthProviders[$appIdKey], appSecret: $oAuthProviders[$appSecretKey], callback: '', state: [], scopes: []);
$providerInstance->verifyCredentials();
$oAuthProviders[$enabledKey] = true;
} catch(\Throwable $err) {
if($enabled === true) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Could not enable OAuth2 provider: ' . $err->getMessage());
}
}
}
$updates = new Document([
'oAuthProviders' => $oAuthProviders
]);
$project = $authorization->skip(fn() => $dbForPlatform->updateDocument('projects', $project->getId(), $updates));
$response->dynamic(new Document([
'$id' => $providerId,
'enabled' => $oAuthProviders[$enabledKey] ?? false,
'clientId' => $oAuthProviders[$appIdKey] ?? '',
'clientSecret' => $oAuthProviders[$appSecretKey] ?? '',
]), Response::MODEL_OAUTH2_GITHUB);
public static function getClientSecretDescription(): string
{
return 'Client secret of GitHub OAuth2 app, or GitHub generic app. For example: 5e07c00000000000000000000000000000198bcc';
}
}