From 7fbfb6266b9f69af7ac308c2b7510f0692c36d0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 24 Apr 2026 10:56:39 +0200 Subject: [PATCH 01/51] GitHub oauth response model --- app/init/models.php | 2 + src/Appwrite/Utopia/Response.php | 1 + .../Utopia/Response/Model/OAuth2Base.php | 19 ++++++++ .../Utopia/Response/Model/OAuth2GitHub.php | 47 +++++++++++++++++++ 4 files changed, 69 insertions(+) create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Base.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php diff --git a/app/init/models.php b/app/init/models.php index b713d61cd2..ed3233e242 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -105,6 +105,7 @@ use Appwrite\Utopia\Response\Model\MigrationReport; use Appwrite\Utopia\Response\Model\Mock; use Appwrite\Utopia\Response\Model\MockNumber; use Appwrite\Utopia\Response\Model\None; +use Appwrite\Utopia\Response\Model\OAuth2GitHub; use Appwrite\Utopia\Response\Model\Phone; use Appwrite\Utopia\Response\Model\PlatformAndroid; use Appwrite\Utopia\Response\Model\PlatformApple; @@ -350,6 +351,7 @@ Response::setModel(new Webhook()); Response::setModel(new Key()); Response::setModel(new DevKey()); Response::setModel(new MockNumber()); +Response::setModel(new OAuth2GitHub()); Response::setModel(new PolicyPasswordDictionary()); Response::setModel(new PolicyPasswordHistory()); Response::setModel(new PolicyPasswordPersonalData()); diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index c4e616ea12..5ca831ed31 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -278,6 +278,7 @@ class Response extends SwooleResponse public const MODEL_VCS = 'vcs'; public const MODEL_EMAIL_TEMPLATE = 'emailTemplate'; public const MODEL_EMAIL_TEMPLATE_LIST = 'emailTemplateList'; + public const MODEL_OAUTH2_GITHUB = 'oAuth2Github'; // Health public const MODEL_HEALTH_STATUS = 'healthStatus'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Base.php b/src/Appwrite/Utopia/Response/Model/OAuth2Base.php new file mode 100644 index 0000000000..f9972e9e50 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Base.php @@ -0,0 +1,19 @@ +addRule('enabled', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'OAuth 2 provider is active and can be used to create sessions.', + 'default' => false, + 'example' => false, + ]); + } +} diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php b/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php new file mode 100644 index 0000000000..b3853b7cc2 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php @@ -0,0 +1,47 @@ +addRule('clientId', [ + 'type' => self::TYPE_STRING, + 'description' => 'GitHub OAuth 2 client ID. For GitHub Apps, use the "App ID" when both an App ID and client ID are available.', + 'default' => '', + 'example' => '123456', + ]) + ->addRule('clientSecret', [ + 'type' => self::TYPE_STRING, + 'description' => 'GitHub OAuth 2 client secret.', + 'default' => '', + 'example' => 'github-client-secret', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'OAuth2GitHub'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_OAUTH2_GITHUB; + } +} From 93f7a0d902ead4ffdfa19de3084daff37d59c35e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 24 Apr 2026 11:17:18 +0200 Subject: [PATCH 02/51] GitHub oauth endpoint --- app/config/roles.php | 2 + app/config/scopes/project.php | 8 + src/Appwrite/Auth/OAuth2.php | 7 + src/Appwrite/Auth/OAuth2/Github.php | 30 ++++ .../Http/Project/OAuth2/GitHub/Update.php | 144 ++++++++++++++++++ .../Modules/Project/Services/Http.php | 4 + src/Appwrite/Platform/Workers/Migrations.php | 2 + tests/benchmarks/http.js | 2 + tests/e2e/Scopes/ProjectCustom.php | 2 + 9 files changed, 201 insertions(+) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php diff --git a/app/config/roles.php b/app/config/roles.php index 33c7ffc9de..d653b4857c 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -55,6 +55,8 @@ $admins = [ 'tables.write', 'platforms.read', 'platforms.write', + 'oauth2.read', + 'oauth2.write', 'mocks.read', 'mocks.write', 'policies.read', diff --git a/app/config/scopes/project.php b/app/config/scopes/project.php index 592e032ba1..947cd863f8 100644 --- a/app/config/scopes/project.php +++ b/app/config/scopes/project.php @@ -228,4 +228,12 @@ return [ // List of publicly visible scopes "description" => "Access to create, update, and delete project\'s templates", ], + "oauth2.read" => [ + "description" => + "Access to read project\'s OAuth2 configuration", + ], + "oauth2.write" => [ + "description" => + "Access to update project\'s OAuth2 configuration", + ], ]; diff --git a/src/Appwrite/Auth/OAuth2.php b/src/Appwrite/Auth/OAuth2.php index a8a2d175b5..3861004498 100644 --- a/src/Appwrite/Auth/OAuth2.php +++ b/src/Appwrite/Auth/OAuth2.php @@ -50,6 +50,13 @@ abstract class OAuth2 $this->addScope($scope); } } + + /** + * Check if the OAuth credentials are valid + * + * @throws \Exception + */ + abstract public function verifyCredentials(): void; /** * @return string diff --git a/src/Appwrite/Auth/OAuth2/Github.php b/src/Appwrite/Auth/OAuth2/Github.php index 1cefc397c5..49d62aa022 100644 --- a/src/Appwrite/Auth/OAuth2/Github.php +++ b/src/Appwrite/Auth/OAuth2/Github.php @@ -1,6 +1,7 @@ addHeader('Accept', 'application/json'); + + $response = $client->fetch( + url: 'https://github.com/login/oauth/access_token', + method: FetchClient::METHOD_POST, + query: [ + 'client_id' => $this->appID, + 'client_secret' => $this->appSecret, + 'code' => 'intentionally-invalid-code', + 'redirect_uri' => 'intentionally-invalid-redirect', + ] + ); + + $json = \json_decode($response->getBody(), true); + + if (isset($json['error']) && $json['error'] === "Not Found") { + throw new \Exception('GitHub application with provided Client ID is does not exist.'); + } + + if (isset($json['error']) && $json['error'] === "incorrect_client_credentials") { + throw new \Exception('GitHub application with provided Client ID is valid, but the provided Client Secret is incorrect.'); + } + + // We still expect error, like redirect_uri_mismatch or bad_verification_code, + // but that indicates valid credentials + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php new file mode 100644 index 0000000000..ffdb2c78d0 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php @@ -0,0 +1,144 @@ +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: <<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(...)); + } + + 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'; + + 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_GITHUB); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index 64dad109f8..b1441be304 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -17,6 +17,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\Get as GetMockPhone use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\Update as UpdateMockPhone; use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\XList as ListMockPhones; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Create as CreateAndroidPlatform; +use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\GitHub\Update as UpdateOAuth2GitHub; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Update as UpdateAndroidPlatform; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple\Create as CreateApplePlatform; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple\Update as UpdateApplePlatform; @@ -129,5 +130,8 @@ class Http extends Service // Auth Methods $this->addAction(UpdateAuthMethod::getName(), new UpdateAuthMethod()); + + // OAuth2 + $this->addAction(UpdateOAuth2GitHub::getName(), new UpdateOAuth2GitHub()); } } diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index cfe8d2d567..fa2ed5883f 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -391,6 +391,8 @@ class Migrations extends Action 'keys.write', 'platforms.read', 'platforms.write', + 'oauth2.read', + 'oauth2.write', 'mocks.read', 'mocks.write', 'policies.read', diff --git a/tests/benchmarks/http.js b/tests/benchmarks/http.js index 4009024069..6466ffd361 100644 --- a/tests/benchmarks/http.js +++ b/tests/benchmarks/http.js @@ -90,6 +90,8 @@ const API_SCOPES = [ 'tokens.write', 'platforms.read', 'platforms.write', + 'oauth2.read', + 'oauth2.write', ]; const BASE_PERMISSIONS = [ diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index f531ed774d..31d85524af 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -169,6 +169,8 @@ trait ProjectCustom 'keys.write', 'platforms.read', 'platforms.write', + 'oauth2.read', + 'oauth2.write', 'mocks.read', 'mocks.write', 'policies.read', From 36435d940dca6147634b35e153bbd4bdd513cdac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 24 Apr 2026 11:35:30 +0200 Subject: [PATCH 03/51] Add Discord OAuth endpoint --- app/init/models.php | 2 + src/Appwrite/Auth/OAuth2/Discord.php | 5 + .../Http/Project/OAuth2/Discord/Update.php | 144 ++++++++++++++++++ .../Modules/Project/Services/Http.php | 2 + src/Appwrite/Utopia/Response.php | 1 + .../Utopia/Response/Model/OAuth2Discord.php | 47 ++++++ 6 files changed, 201 insertions(+) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Discord.php diff --git a/app/init/models.php b/app/init/models.php index ed3233e242..9b31d17171 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -105,6 +105,7 @@ use Appwrite\Utopia\Response\Model\MigrationReport; use Appwrite\Utopia\Response\Model\Mock; use Appwrite\Utopia\Response\Model\MockNumber; use Appwrite\Utopia\Response\Model\None; +use Appwrite\Utopia\Response\Model\OAuth2Discord; use Appwrite\Utopia\Response\Model\OAuth2GitHub; use Appwrite\Utopia\Response\Model\Phone; use Appwrite\Utopia\Response\Model\PlatformAndroid; @@ -352,6 +353,7 @@ Response::setModel(new Key()); Response::setModel(new DevKey()); Response::setModel(new MockNumber()); Response::setModel(new OAuth2GitHub()); +Response::setModel(new OAuth2Discord()); Response::setModel(new PolicyPasswordDictionary()); Response::setModel(new PolicyPasswordHistory()); Response::setModel(new PolicyPasswordPersonalData()); diff --git a/src/Appwrite/Auth/OAuth2/Discord.php b/src/Appwrite/Auth/OAuth2/Discord.php index a5ecdb5e3c..ede5ce36c2 100644 --- a/src/Appwrite/Auth/OAuth2/Discord.php +++ b/src/Appwrite/Auth/OAuth2/Discord.php @@ -1,6 +1,7 @@ user; } + + public function verifyCredentials(): void { + // TODO: Implement, eventuelly. Refer to GitHub.php in this directory for inspiration + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php new file mode 100644 index 0000000000..091cc41637 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php @@ -0,0 +1,144 @@ +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: <<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(...)); + } + + 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'; + + 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); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index b1441be304..f69c9fa0ef 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -16,6 +16,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\Delete as DeleteMoc use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\Get as GetMockPhone; use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\Update as UpdateMockPhone; use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\XList as ListMockPhones; +use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Discord\Update as UpdateOAuth2Discord; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Create as CreateAndroidPlatform; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\GitHub\Update as UpdateOAuth2GitHub; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Update as UpdateAndroidPlatform; @@ -133,5 +134,6 @@ class Http extends Service // OAuth2 $this->addAction(UpdateOAuth2GitHub::getName(), new UpdateOAuth2GitHub()); + $this->addAction(UpdateOAuth2Discord::getName(), new UpdateOAuth2Discord()); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 5ca831ed31..2eb774a630 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -279,6 +279,7 @@ class Response extends SwooleResponse public const MODEL_EMAIL_TEMPLATE = 'emailTemplate'; public const MODEL_EMAIL_TEMPLATE_LIST = 'emailTemplateList'; public const MODEL_OAUTH2_GITHUB = 'oAuth2Github'; + public const MODEL_OAUTH2_DISCORD = 'oAuth2Discord'; // Health public const MODEL_HEALTH_STATUS = 'healthStatus'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Discord.php b/src/Appwrite/Utopia/Response/Model/OAuth2Discord.php new file mode 100644 index 0000000000..cd2b0b74e2 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Discord.php @@ -0,0 +1,47 @@ +addRule('clientId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Discord OAuth 2 client ID.', + 'default' => '', + 'example' => '950722000000343754', + ]) + ->addRule('clientSecret', [ + 'type' => self::TYPE_STRING, + 'description' => 'Discord OAuth 2 client secret.', + 'default' => '', + 'example' => 'YmPXnM000000000000000000002zFg5D', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'OAuth2Discord'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_OAUTH2_DISCORD; + } +} From 5fbe6cba79b3eee3f3f8663db3f045c426af227a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 24 Apr 2026 11:39:14 +0200 Subject: [PATCH 04/51] Improve github samples --- src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php b/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php index b3853b7cc2..27b529aedd 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php @@ -15,13 +15,13 @@ class OAuth2GitHub extends OAuth2Base 'type' => self::TYPE_STRING, 'description' => 'GitHub OAuth 2 client ID. For GitHub Apps, use the "App ID" when both an App ID and client ID are available.', 'default' => '', - 'example' => '123456', + 'example' => 'e4d87900000000540733', ]) ->addRule('clientSecret', [ 'type' => self::TYPE_STRING, 'description' => 'GitHub OAuth 2 client secret.', 'default' => '', - 'example' => 'github-client-secret', + 'example' => '5e07c00000000000000000000000000000198bcc', ]); } From 335b1c2f6ccea1d7f3ba700e666faecca1344748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 24 Apr 2026 11:45:59 +0200 Subject: [PATCH 05/51] Figma OAuth endpoint --- app/init/models.php | 2 + src/Appwrite/Auth/OAuth2/Figma.php | 4 + .../Http/Project/OAuth2/Figma/Update.php | 144 ++++++++++++++++++ .../Modules/Project/Services/Http.php | 2 + src/Appwrite/Utopia/Response.php | 1 + .../Utopia/Response/Model/OAuth2Figma.php | 47 ++++++ 6 files changed, 200 insertions(+) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Figma.php diff --git a/app/init/models.php b/app/init/models.php index 9b31d17171..46e758d5b2 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -106,6 +106,7 @@ use Appwrite\Utopia\Response\Model\Mock; use Appwrite\Utopia\Response\Model\MockNumber; use Appwrite\Utopia\Response\Model\None; use Appwrite\Utopia\Response\Model\OAuth2Discord; +use Appwrite\Utopia\Response\Model\OAuth2Figma; use Appwrite\Utopia\Response\Model\OAuth2GitHub; use Appwrite\Utopia\Response\Model\Phone; use Appwrite\Utopia\Response\Model\PlatformAndroid; @@ -354,6 +355,7 @@ Response::setModel(new DevKey()); Response::setModel(new MockNumber()); Response::setModel(new OAuth2GitHub()); Response::setModel(new OAuth2Discord()); +Response::setModel(new OAuth2Figma()); Response::setModel(new PolicyPasswordDictionary()); Response::setModel(new PolicyPasswordHistory()); Response::setModel(new PolicyPasswordPersonalData()); diff --git a/src/Appwrite/Auth/OAuth2/Figma.php b/src/Appwrite/Auth/OAuth2/Figma.php index b5e53cbed4..b6ce166e6b 100644 --- a/src/Appwrite/Auth/OAuth2/Figma.php +++ b/src/Appwrite/Auth/OAuth2/Figma.php @@ -175,4 +175,8 @@ class Figma extends OAuth2 return $this->user; } + + public function verifyCredentials(): void { + // TODO: Implement, eventuelly. Refer to GitHub.php in this directory for inspiration + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php new file mode 100644 index 0000000000..34ec34be9d --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php @@ -0,0 +1,144 @@ +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: <<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(...)); + } + + 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'; + + 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); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index f69c9fa0ef..8d1de316a4 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -17,6 +17,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\Get as GetMockPhone use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\Update as UpdateMockPhone; use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\XList as ListMockPhones; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Discord\Update as UpdateOAuth2Discord; +use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Figma\Update as UpdateOAuth2Figma; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Create as CreateAndroidPlatform; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\GitHub\Update as UpdateOAuth2GitHub; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Update as UpdateAndroidPlatform; @@ -135,5 +136,6 @@ class Http extends Service // OAuth2 $this->addAction(UpdateOAuth2GitHub::getName(), new UpdateOAuth2GitHub()); $this->addAction(UpdateOAuth2Discord::getName(), new UpdateOAuth2Discord()); + $this->addAction(UpdateOAuth2Figma::getName(), new UpdateOAuth2Figma()); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 2eb774a630..820ec8f75f 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -280,6 +280,7 @@ class Response extends SwooleResponse public const MODEL_EMAIL_TEMPLATE_LIST = 'emailTemplateList'; public const MODEL_OAUTH2_GITHUB = 'oAuth2Github'; public const MODEL_OAUTH2_DISCORD = 'oAuth2Discord'; + public const MODEL_OAUTH2_FIGMA = 'oAuth2Figma'; // Health public const MODEL_HEALTH_STATUS = 'healthStatus'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Figma.php b/src/Appwrite/Utopia/Response/Model/OAuth2Figma.php new file mode 100644 index 0000000000..2ee60adaa8 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Figma.php @@ -0,0 +1,47 @@ +addRule('clientId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Figma OAuth 2 client ID.', + 'default' => '', + 'example' => 'byay5H0000000000VtiI40', + ]) + ->addRule('clientSecret', [ + 'type' => self::TYPE_STRING, + 'description' => 'Figma OAuth 2 client secret.', + 'default' => '', + 'example' => 'yEpOYn0000000000000000004iIsU5', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'OAuth2Figma'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_OAUTH2_FIGMA; + } +} From dac184b281fd01b908edb6859bb84c7fee7f2a3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 24 Apr 2026 12:06:58 +0200 Subject: [PATCH 06/51] abstract oauth adapters --- src/Appwrite/Auth/OAuth2.php | 7 - src/Appwrite/Auth/OAuth2/Discord.php | 4 - src/Appwrite/Auth/OAuth2/Figma.php | 4 - .../Project/Http/Project/OAuth2/Base.php | 175 ++++++++++++++++++ .../Http/Project/OAuth2/Discord/Update.php | 134 ++------------ .../Http/Project/OAuth2/Figma/Update.php | 134 ++------------ .../Http/Project/OAuth2/GitHub/Update.php | 136 ++------------ 7 files changed, 221 insertions(+), 373 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php diff --git a/src/Appwrite/Auth/OAuth2.php b/src/Appwrite/Auth/OAuth2.php index 3861004498..a8a2d175b5 100644 --- a/src/Appwrite/Auth/OAuth2.php +++ b/src/Appwrite/Auth/OAuth2.php @@ -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 diff --git a/src/Appwrite/Auth/OAuth2/Discord.php b/src/Appwrite/Auth/OAuth2/Discord.php index ede5ce36c2..6cb682479a 100644 --- a/src/Appwrite/Auth/OAuth2/Discord.php +++ b/src/Appwrite/Auth/OAuth2/Discord.php @@ -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 - } } diff --git a/src/Appwrite/Auth/OAuth2/Figma.php b/src/Appwrite/Auth/OAuth2/Figma.php index b6ce166e6b..b5e53cbed4 100644 --- a/src/Appwrite/Auth/OAuth2/Figma.php +++ b/src/Appwrite/Auth/OAuth2/Figma.php @@ -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 - } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php new file mode 100644 index 0000000000..e2cc405a59 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php @@ -0,0 +1,175 @@ +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()); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php index 091cc41637..383aee12d6 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php @@ -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: <<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'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php index 34ec34be9d..c19b9fb30f 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php @@ -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: <<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'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php index ffdb2c78d0..4490fa90cd 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php @@ -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: <<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'; } } From c097d9fcdd7fb70d57750cfede5b1028e5e45c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 24 Apr 2026 12:20:48 +0200 Subject: [PATCH 07/51] Dropbox adapter --- app/init/models.php | 2 + .../Project/Http/Project/OAuth2/Base.php | 33 ++++++++++-- .../Http/Project/OAuth2/Dropbox/Update.php | 50 +++++++++++++++++++ .../Modules/Project/Services/Http.php | 2 + src/Appwrite/Utopia/Response.php | 1 + .../Utopia/Response/Model/OAuth2Dropbox.php | 47 +++++++++++++++++ 6 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Dropbox.php diff --git a/app/init/models.php b/app/init/models.php index 46e758d5b2..5c2910786d 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -106,6 +106,7 @@ use Appwrite\Utopia\Response\Model\Mock; use Appwrite\Utopia\Response\Model\MockNumber; use Appwrite\Utopia\Response\Model\None; use Appwrite\Utopia\Response\Model\OAuth2Discord; +use Appwrite\Utopia\Response\Model\OAuth2Dropbox; use Appwrite\Utopia\Response\Model\OAuth2Figma; use Appwrite\Utopia\Response\Model\OAuth2GitHub; use Appwrite\Utopia\Response\Model\Phone; @@ -356,6 +357,7 @@ Response::setModel(new MockNumber()); Response::setModel(new OAuth2GitHub()); Response::setModel(new OAuth2Discord()); Response::setModel(new OAuth2Figma()); +Response::setModel(new OAuth2Dropbox()); Response::setModel(new PolicyPasswordDictionary()); Response::setModel(new PolicyPasswordHistory()); Response::setModel(new PolicyPasswordPersonalData()); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php index e2cc405a59..b40b0f06e8 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php @@ -63,6 +63,31 @@ abstract class Base extends Action */ abstract public static function getClientSecretDescription(): string; + /** + * Public-facing name of the clientId param. Some providers use a different + * terminology (e.g. Dropbox calls it "App key"), so the param name and the + * corresponding response field can be customized by overriding this method. + * + * @return string e.g. 'clientId' (default), 'appKey' + */ + public static function getClientIdParamName(): string + { + return 'clientId'; + } + + /** + * Public-facing name of the clientSecret param. Some providers use a + * different terminology (e.g. Dropbox calls it "App secret"), so the param + * name and the corresponding response field can be customized by + * overriding this method. + * + * @return string e.g. 'clientSecret' (default), 'appSecret' + */ + public static function getClientSecretParamName(): string + { + return 'clientSecret'; + } + public static function getName() { return 'updateProjectOAuth2' . static::getProviderLabel(); @@ -95,8 +120,8 @@ abstract class Base extends Action ) ], )) - ->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(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) + ->param(static::getClientSecretParamName(), 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') @@ -168,8 +193,8 @@ abstract class Base extends Action $response->dynamic(new Document([ '$id' => $providerId, 'enabled' => $oAuthProviders[$enabledKey] ?? false, - 'clientId' => $oAuthProviders[$appIdKey] ?? '', - 'clientSecret' => $oAuthProviders[$appSecretKey] ?? '', + static::getClientIdParamName() => $oAuthProviders[$appIdKey] ?? '', + static::getClientSecretParamName() => $oAuthProviders[$appSecretKey] ?? '', ]), static::getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php new file mode 100644 index 0000000000..6cc34cc612 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php @@ -0,0 +1,50 @@ +addAction(UpdateOAuth2GitHub::getName(), new UpdateOAuth2GitHub()); $this->addAction(UpdateOAuth2Discord::getName(), new UpdateOAuth2Discord()); $this->addAction(UpdateOAuth2Figma::getName(), new UpdateOAuth2Figma()); + $this->addAction(UpdateOAuth2Dropbox::getName(), new UpdateOAuth2Dropbox()); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 820ec8f75f..36ab89d012 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -281,6 +281,7 @@ class Response extends SwooleResponse public const MODEL_OAUTH2_GITHUB = 'oAuth2Github'; public const MODEL_OAUTH2_DISCORD = 'oAuth2Discord'; public const MODEL_OAUTH2_FIGMA = 'oAuth2Figma'; + public const MODEL_OAUTH2_DROPBOX = 'oAuth2Dropbox'; // Health public const MODEL_HEALTH_STATUS = 'healthStatus'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Dropbox.php b/src/Appwrite/Utopia/Response/Model/OAuth2Dropbox.php new file mode 100644 index 0000000000..9289168bcc --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Dropbox.php @@ -0,0 +1,47 @@ +addRule('appKey', [ + 'type' => self::TYPE_STRING, + 'description' => 'Dropbox OAuth 2 app key.', + 'default' => '', + 'example' => 'jl000000000009t', + ]) + ->addRule('appSecret', [ + 'type' => self::TYPE_STRING, + 'description' => 'Dropbox OAuth 2 app secret.', + 'default' => '', + 'example' => 'g200000000000vw', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'OAuth2Dropbox'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_OAUTH2_DROPBOX; + } +} From faf09ed7c57270b8de57f874331fb8631484c5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 24 Apr 2026 12:38:12 +0200 Subject: [PATCH 08/51] Abstrated oauth response model --- .../Utopia/Response/Model/OAuth2Base.php | 100 ++++++++++++++++++ .../Utopia/Response/Model/OAuth2Discord.php | 26 ++--- .../Utopia/Response/Model/OAuth2Dropbox.php | 46 +++++--- .../Utopia/Response/Model/OAuth2Figma.php | 26 ++--- .../Utopia/Response/Model/OAuth2GitHub.php | 31 +++--- 5 files changed, 169 insertions(+), 60 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Base.php b/src/Appwrite/Utopia/Response/Model/OAuth2Base.php index f9972e9e50..b0bd642b34 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Base.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Base.php @@ -6,6 +6,94 @@ use Appwrite\Utopia\Response\Model; abstract class OAuth2Base extends Model { + /** + * Provider display label used in rule descriptions. + * + * @return string e.g. 'GitHub', 'Discord', 'Dropbox' + */ + abstract public function getProviderLabel(): string; + + /** + * Example value for the client ID rule. + * + * @return string + */ + abstract public function getClientIdExample(): string; + + /** + * Example value for the client secret rule. + * + * @return string + */ + abstract public function getClientSecretExample(): string; + + /** + * Public-facing field name of the client ID. Providers may override when + * they use different terminology (e.g. Dropbox -> 'appKey'). + * + * @return string + */ + public function getClientIdFieldName(): string + { + return 'clientId'; + } + + /** + * Public-facing field name of the client secret. Providers may override + * when they use different terminology (e.g. Dropbox -> 'appSecret'). + * + * @return string + */ + public function getClientSecretFieldName(): string + { + return 'clientSecret'; + } + + /** + * Human-readable label for the client ID, used in the generated rule + * description. Providers may override (e.g. Dropbox -> 'app key'). + * + * @return string + */ + public function getClientIdLabel(): string + { + return 'client ID'; + } + + /** + * Human-readable label for the client secret, used in the generated rule + * description. Providers may override (e.g. Dropbox -> 'app secret'). + * + * @return string + */ + public function getClientSecretLabel(): string + { + return 'client secret'; + } + + /** + * Rule description for the client ID. Auto-generated from the provider + * label and client ID label. Providers may override to add extra context. + * + * @return string + */ + public function getClientIdDescription(): string + { + return $this->getProviderLabel() . ' OAuth 2 ' . $this->getClientIdLabel() . '.'; + } + + /** + * Rule description for the client secret. Auto-generated from the provider + * label and client secret label. Providers may override to add extra + * context. + * + * @return string + */ + public function getClientSecretDescription(): string + { + return $this->getProviderLabel() . ' OAuth 2 ' . $this->getClientSecretLabel() . '.'; + } + public function __construct() { $this @@ -14,6 +102,18 @@ abstract class OAuth2Base extends Model 'description' => 'OAuth 2 provider is active and can be used to create sessions.', 'default' => false, 'example' => false, + ]) + ->addRule($this->getClientIdFieldName(), [ + 'type' => self::TYPE_STRING, + 'description' => $this->getClientIdDescription(), + 'default' => '', + 'example' => $this->getClientIdExample(), + ]) + ->addRule($this->getClientSecretFieldName(), [ + 'type' => self::TYPE_STRING, + 'description' => $this->getClientSecretDescription(), + 'default' => '', + 'example' => $this->getClientSecretExample(), ]); } } diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Discord.php b/src/Appwrite/Utopia/Response/Model/OAuth2Discord.php index cd2b0b74e2..da7c4873b5 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Discord.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Discord.php @@ -6,23 +6,19 @@ use Appwrite\Utopia\Response; class OAuth2Discord extends OAuth2Base { - public function __construct() + public function getProviderLabel(): string { - parent::__construct(); + return 'Discord'; + } - $this - ->addRule('clientId', [ - 'type' => self::TYPE_STRING, - 'description' => 'Discord OAuth 2 client ID.', - 'default' => '', - 'example' => '950722000000343754', - ]) - ->addRule('clientSecret', [ - 'type' => self::TYPE_STRING, - 'description' => 'Discord OAuth 2 client secret.', - 'default' => '', - 'example' => 'YmPXnM000000000000000000002zFg5D', - ]); + public function getClientIdExample(): string + { + return '950722000000343754'; + } + + public function getClientSecretExample(): string + { + return 'YmPXnM000000000000000000002zFg5D'; } /** diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Dropbox.php b/src/Appwrite/Utopia/Response/Model/OAuth2Dropbox.php index 9289168bcc..4924db1397 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Dropbox.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Dropbox.php @@ -6,23 +6,39 @@ use Appwrite\Utopia\Response; class OAuth2Dropbox extends OAuth2Base { - public function __construct() + public function getProviderLabel(): string { - parent::__construct(); + return 'Dropbox'; + } - $this - ->addRule('appKey', [ - 'type' => self::TYPE_STRING, - 'description' => 'Dropbox OAuth 2 app key.', - 'default' => '', - 'example' => 'jl000000000009t', - ]) - ->addRule('appSecret', [ - 'type' => self::TYPE_STRING, - 'description' => 'Dropbox OAuth 2 app secret.', - 'default' => '', - 'example' => 'g200000000000vw', - ]); + public function getClientIdExample(): string + { + return 'jl000000000009t'; + } + + public function getClientSecretExample(): string + { + return 'g200000000000vw'; + } + + public function getClientIdFieldName(): string + { + return 'appKey'; + } + + public function getClientSecretFieldName(): string + { + return 'appSecret'; + } + + public function getClientIdLabel(): string + { + return 'app key'; + } + + public function getClientSecretLabel(): string + { + return 'app secret'; } /** diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Figma.php b/src/Appwrite/Utopia/Response/Model/OAuth2Figma.php index 2ee60adaa8..533d353d01 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Figma.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Figma.php @@ -6,23 +6,19 @@ use Appwrite\Utopia\Response; class OAuth2Figma extends OAuth2Base { - public function __construct() + public function getProviderLabel(): string { - parent::__construct(); + return 'Figma'; + } - $this - ->addRule('clientId', [ - 'type' => self::TYPE_STRING, - 'description' => 'Figma OAuth 2 client ID.', - 'default' => '', - 'example' => 'byay5H0000000000VtiI40', - ]) - ->addRule('clientSecret', [ - 'type' => self::TYPE_STRING, - 'description' => 'Figma OAuth 2 client secret.', - 'default' => '', - 'example' => 'yEpOYn0000000000000000004iIsU5', - ]); + public function getClientIdExample(): string + { + return 'byay5H0000000000VtiI40'; + } + + public function getClientSecretExample(): string + { + return 'yEpOYn0000000000000000004iIsU5'; } /** diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php b/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php index 27b529aedd..30d3a71187 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php @@ -6,23 +6,24 @@ use Appwrite\Utopia\Response; class OAuth2GitHub extends OAuth2Base { - public function __construct() + public function getProviderLabel(): string { - parent::__construct(); + return 'GitHub'; + } - $this - ->addRule('clientId', [ - 'type' => self::TYPE_STRING, - 'description' => 'GitHub OAuth 2 client ID. For GitHub Apps, use the "App ID" when both an App ID and client ID are available.', - 'default' => '', - 'example' => 'e4d87900000000540733', - ]) - ->addRule('clientSecret', [ - 'type' => self::TYPE_STRING, - 'description' => 'GitHub OAuth 2 client secret.', - 'default' => '', - 'example' => '5e07c00000000000000000000000000000198bcc', - ]); + public function getClientIdExample(): string + { + return 'e4d87900000000540733'; + } + + public function getClientSecretExample(): string + { + return '5e07c00000000000000000000000000000198bcc'; + } + + public function getClientIdDescription(): string + { + return parent::getClientIdDescription() . ' For GitHub Apps, use the "App ID" when both an App ID and client ID are available.'; } /** From fe08978851cc8e4656fc1a036091e822664e1dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 24 Apr 2026 12:58:32 +0200 Subject: [PATCH 09/51] More OAuth provider endpoints --- app/init/models.php | 12 ++++ .../Http/Project/OAuth2/Autodesk/Update.php | 40 ++++++++++++ .../Http/Project/OAuth2/Bitbucket/Update.php | 50 +++++++++++++++ .../Http/Project/OAuth2/Bitly/Update.php | 40 ++++++++++++ .../Http/Project/OAuth2/Box/Update.php | 40 ++++++++++++ .../Project/OAuth2/Dailymotion/Update.php | 50 +++++++++++++++ .../Http/Project/OAuth2/Google/Update.php | 40 ++++++++++++ .../Modules/Project/Services/Http.php | 12 ++++ src/Appwrite/Utopia/Response.php | 6 ++ .../Utopia/Response/Model/OAuth2Autodesk.php | 43 +++++++++++++ .../Utopia/Response/Model/OAuth2Bitbucket.php | 63 +++++++++++++++++++ .../Utopia/Response/Model/OAuth2Bitly.php | 43 +++++++++++++ .../Utopia/Response/Model/OAuth2Box.php | 43 +++++++++++++ .../Response/Model/OAuth2Dailymotion.php | 63 +++++++++++++++++++ .../Utopia/Response/Model/OAuth2Google.php | 43 +++++++++++++ 15 files changed, 588 insertions(+) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Autodesk.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Bitbucket.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Bitly.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Box.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Dailymotion.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Google.php diff --git a/app/init/models.php b/app/init/models.php index 5c2910786d..da872b5d7b 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -105,10 +105,16 @@ use Appwrite\Utopia\Response\Model\MigrationReport; use Appwrite\Utopia\Response\Model\Mock; use Appwrite\Utopia\Response\Model\MockNumber; use Appwrite\Utopia\Response\Model\None; +use Appwrite\Utopia\Response\Model\OAuth2Autodesk; +use Appwrite\Utopia\Response\Model\OAuth2Bitbucket; +use Appwrite\Utopia\Response\Model\OAuth2Bitly; +use Appwrite\Utopia\Response\Model\OAuth2Box; +use Appwrite\Utopia\Response\Model\OAuth2Dailymotion; use Appwrite\Utopia\Response\Model\OAuth2Discord; use Appwrite\Utopia\Response\Model\OAuth2Dropbox; use Appwrite\Utopia\Response\Model\OAuth2Figma; use Appwrite\Utopia\Response\Model\OAuth2GitHub; +use Appwrite\Utopia\Response\Model\OAuth2Google; use Appwrite\Utopia\Response\Model\Phone; use Appwrite\Utopia\Response\Model\PlatformAndroid; use Appwrite\Utopia\Response\Model\PlatformApple; @@ -358,6 +364,12 @@ Response::setModel(new OAuth2GitHub()); Response::setModel(new OAuth2Discord()); Response::setModel(new OAuth2Figma()); Response::setModel(new OAuth2Dropbox()); +Response::setModel(new OAuth2Dailymotion()); +Response::setModel(new OAuth2Bitbucket()); +Response::setModel(new OAuth2Bitly()); +Response::setModel(new OAuth2Box()); +Response::setModel(new OAuth2Autodesk()); +Response::setModel(new OAuth2Google()); Response::setModel(new PolicyPasswordDictionary()); Response::setModel(new PolicyPasswordHistory()); Response::setModel(new PolicyPasswordPersonalData()); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php new file mode 100644 index 0000000000..29eaacdc87 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php @@ -0,0 +1,40 @@ +addAction(UpdateOAuth2Discord::getName(), new UpdateOAuth2Discord()); $this->addAction(UpdateOAuth2Figma::getName(), new UpdateOAuth2Figma()); $this->addAction(UpdateOAuth2Dropbox::getName(), new UpdateOAuth2Dropbox()); + $this->addAction(UpdateOAuth2Dailymotion::getName(), new UpdateOAuth2Dailymotion()); + $this->addAction(UpdateOAuth2Bitbucket::getName(), new UpdateOAuth2Bitbucket()); + $this->addAction(UpdateOAuth2Bitly::getName(), new UpdateOAuth2Bitly()); + $this->addAction(UpdateOAuth2Box::getName(), new UpdateOAuth2Box()); + $this->addAction(UpdateOAuth2Autodesk::getName(), new UpdateOAuth2Autodesk()); + $this->addAction(UpdateOAuth2Google::getName(), new UpdateOAuth2Google()); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 36ab89d012..dc315d83fd 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -282,6 +282,12 @@ class Response extends SwooleResponse public const MODEL_OAUTH2_DISCORD = 'oAuth2Discord'; public const MODEL_OAUTH2_FIGMA = 'oAuth2Figma'; public const MODEL_OAUTH2_DROPBOX = 'oAuth2Dropbox'; + public const MODEL_OAUTH2_DAILYMOTION = 'oAuth2Dailymotion'; + public const MODEL_OAUTH2_BITBUCKET = 'oAuth2Bitbucket'; + public const MODEL_OAUTH2_BITLY = 'oAuth2Bitly'; + public const MODEL_OAUTH2_BOX = 'oAuth2Box'; + public const MODEL_OAUTH2_AUTODESK = 'oAuth2Autodesk'; + public const MODEL_OAUTH2_GOOGLE = 'oAuth2Google'; // Health public const MODEL_HEALTH_STATUS = 'healthStatus'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Autodesk.php b/src/Appwrite/Utopia/Response/Model/OAuth2Autodesk.php new file mode 100644 index 0000000000..6f55b5d475 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Autodesk.php @@ -0,0 +1,43 @@ + Date: Fri, 24 Apr 2026 14:15:34 +0200 Subject: [PATCH 10/51] Add more oauth endpoints --- analyze.sh | 75 +++++++++++++++++++ app/init/models.php | 36 +++++++++ src/Appwrite/Auth/OAuth2/Discord.php | 1 - src/Appwrite/Auth/OAuth2/Github.php | 19 ++--- .../Http/Project/OAuth2/Amazon/Update.php | 40 ++++++++++ .../Project/Http/Project/OAuth2/Base.php | 14 ++-- .../Http/Project/OAuth2/Disqus/Update.php | 50 +++++++++++++ .../Http/Project/OAuth2/Etsy/Update.php | 50 +++++++++++++ .../Http/Project/OAuth2/Facebook/Update.php | 50 +++++++++++++ .../Http/Project/OAuth2/Linkedin/Update.php | 45 +++++++++++ .../Http/Project/OAuth2/Notion/Update.php | 50 +++++++++++++ .../Http/Project/OAuth2/Podio/Update.php | 40 ++++++++++ .../Http/Project/OAuth2/Salesforce/Update.php | 50 +++++++++++++ .../Http/Project/OAuth2/Slack/Update.php | 40 ++++++++++ .../Http/Project/OAuth2/Spotify/Update.php | 40 ++++++++++ .../Http/Project/OAuth2/Stripe/Update.php | 45 +++++++++++ .../Http/Project/OAuth2/Twitch/Update.php | 40 ++++++++++ .../Http/Project/OAuth2/WordPress/Update.php | 40 ++++++++++ .../Project/Http/Project/OAuth2/X/Update.php | 50 +++++++++++++ .../Http/Project/OAuth2/Yahoo/Update.php | 40 ++++++++++ .../Http/Project/OAuth2/Yandex/Update.php | 40 ++++++++++ .../Http/Project/OAuth2/Zoho/Update.php | 40 ++++++++++ .../Http/Project/OAuth2/Zoom/Update.php | 40 ++++++++++ .../Modules/Project/Services/Http.php | 40 +++++++++- src/Appwrite/Utopia/Response.php | 18 +++++ .../Utopia/Response/Model/OAuth2Amazon.php | 43 +++++++++++ .../Utopia/Response/Model/OAuth2Disqus.php | 63 ++++++++++++++++ .../Utopia/Response/Model/OAuth2Etsy.php | 63 ++++++++++++++++ .../Utopia/Response/Model/OAuth2Facebook.php | 63 ++++++++++++++++ .../Utopia/Response/Model/OAuth2Linkedin.php | 53 +++++++++++++ .../Utopia/Response/Model/OAuth2Notion.php | 53 +++++++++++++ .../Utopia/Response/Model/OAuth2Podio.php | 43 +++++++++++ .../Response/Model/OAuth2Salesforce.php | 63 ++++++++++++++++ .../Utopia/Response/Model/OAuth2Slack.php | 43 +++++++++++ .../Utopia/Response/Model/OAuth2Spotify.php | 43 +++++++++++ .../Utopia/Response/Model/OAuth2Stripe.php | 53 +++++++++++++ .../Utopia/Response/Model/OAuth2Twitch.php | 43 +++++++++++ .../Utopia/Response/Model/OAuth2WordPress.php | 43 +++++++++++ .../Utopia/Response/Model/OAuth2X.php | 63 ++++++++++++++++ .../Utopia/Response/Model/OAuth2Yahoo.php | 43 +++++++++++ .../Utopia/Response/Model/OAuth2Yandex.php | 43 +++++++++++ .../Utopia/Response/Model/OAuth2Zoho.php | 43 +++++++++++ .../Utopia/Response/Model/OAuth2Zoom.php | 43 +++++++++++ 43 files changed, 1878 insertions(+), 19 deletions(-) create mode 100755 analyze.sh create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Twitch/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/WordPress/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/X/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yahoo/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yandex/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoho/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoom/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Amazon.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Disqus.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Etsy.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Facebook.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Linkedin.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Notion.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Podio.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Salesforce.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Slack.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Spotify.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Stripe.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Twitch.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2WordPress.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2X.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Yahoo.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Yandex.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Zoho.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Zoom.php diff --git a/analyze.sh b/analyze.sh new file mode 100755 index 0000000000..1620e9bb73 --- /dev/null +++ b/analyze.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT="/Users/matejbaco/Documents/GitHub/appwrite" +ENDPOINT_DIR="$ROOT/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2" +CONFIG_FILE="$ROOT/app/config/oAuthProviders.php" + +if ! command -v php >/dev/null 2>&1; then + echo "php is required but was not found in PATH" >&2 + exit 1 +fi + +if [[ ! -d "$ENDPOINT_DIR" ]]; then + echo "Endpoint directory not found: $ENDPOINT_DIR" >&2 + exit 1 +fi + +if [[ ! -f "$CONFIG_FILE" ]]; then + echo "Config file not found: $CONFIG_FILE" >&2 + exit 1 +fi + +echo "OAuth2 endpoint files:" +find "$ENDPOINT_DIR" -type f | sort +echo + +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +endpoint_file="$tmp_dir/endpoint-providers.txt" +config_file="$tmp_dir/config-providers.txt" + +find "$ENDPOINT_DIR" -mindepth 2 -maxdepth 2 -type f -name 'Update.php' \ + | while read -r file; do + basename "$(dirname "$file")" | tr '[:upper:]' '[:lower:]' + done \ + | sort -u > "$endpoint_file" + +php -r ' + $providers = require $argv[1]; + $names = []; + + foreach ($providers as $provider) { + if (($provider["mock"] ?? false) === true) { + continue; + } + + $class = $provider["class"] ?? ""; + if ($class === "") { + continue; + } + + $base = substr($class, strrpos($class, "\\") + 1); + $names[strtolower($base)] = true; + } + + $names = array_keys($names); + sort($names); + + foreach ($names as $name) { + echo $name, PHP_EOL; + } +' "$CONFIG_FILE" > "$config_file" + +echo "Configured provider classes:" +cat "$config_file" +echo + +echo "Endpoint provider directories:" +cat "$endpoint_file" +echo + +echo "Configured providers without endpoint:" +comm -23 "$config_file" "$endpoint_file" diff --git a/app/init/models.php b/app/init/models.php index da872b5d7b..df0d0d28d8 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -105,16 +105,34 @@ use Appwrite\Utopia\Response\Model\MigrationReport; use Appwrite\Utopia\Response\Model\Mock; use Appwrite\Utopia\Response\Model\MockNumber; use Appwrite\Utopia\Response\Model\None; +use Appwrite\Utopia\Response\Model\OAuth2Amazon; use Appwrite\Utopia\Response\Model\OAuth2Autodesk; use Appwrite\Utopia\Response\Model\OAuth2Bitbucket; use Appwrite\Utopia\Response\Model\OAuth2Bitly; use Appwrite\Utopia\Response\Model\OAuth2Box; use Appwrite\Utopia\Response\Model\OAuth2Dailymotion; use Appwrite\Utopia\Response\Model\OAuth2Discord; +use Appwrite\Utopia\Response\Model\OAuth2Disqus; use Appwrite\Utopia\Response\Model\OAuth2Dropbox; +use Appwrite\Utopia\Response\Model\OAuth2Etsy; +use Appwrite\Utopia\Response\Model\OAuth2Facebook; use Appwrite\Utopia\Response\Model\OAuth2Figma; use Appwrite\Utopia\Response\Model\OAuth2GitHub; use Appwrite\Utopia\Response\Model\OAuth2Google; +use Appwrite\Utopia\Response\Model\OAuth2Linkedin; +use Appwrite\Utopia\Response\Model\OAuth2Notion; +use Appwrite\Utopia\Response\Model\OAuth2Podio; +use Appwrite\Utopia\Response\Model\OAuth2Salesforce; +use Appwrite\Utopia\Response\Model\OAuth2Slack; +use Appwrite\Utopia\Response\Model\OAuth2Spotify; +use Appwrite\Utopia\Response\Model\OAuth2Stripe; +use Appwrite\Utopia\Response\Model\OAuth2Twitch; +use Appwrite\Utopia\Response\Model\OAuth2WordPress; +use Appwrite\Utopia\Response\Model\OAuth2X; +use Appwrite\Utopia\Response\Model\OAuth2Yahoo; +use Appwrite\Utopia\Response\Model\OAuth2Yandex; +use Appwrite\Utopia\Response\Model\OAuth2Zoho; +use Appwrite\Utopia\Response\Model\OAuth2Zoom; use Appwrite\Utopia\Response\Model\Phone; use Appwrite\Utopia\Response\Model\PlatformAndroid; use Appwrite\Utopia\Response\Model\PlatformApple; @@ -370,6 +388,24 @@ Response::setModel(new OAuth2Bitly()); Response::setModel(new OAuth2Box()); Response::setModel(new OAuth2Autodesk()); Response::setModel(new OAuth2Google()); +Response::setModel(new OAuth2Zoom()); +Response::setModel(new OAuth2Zoho()); +Response::setModel(new OAuth2Yandex()); +Response::setModel(new OAuth2X()); +Response::setModel(new OAuth2WordPress()); +Response::setModel(new OAuth2Twitch()); +Response::setModel(new OAuth2Stripe()); +Response::setModel(new OAuth2Spotify()); +Response::setModel(new OAuth2Slack()); +Response::setModel(new OAuth2Podio()); +Response::setModel(new OAuth2Notion()); +Response::setModel(new OAuth2Salesforce()); +Response::setModel(new OAuth2Yahoo()); +Response::setModel(new OAuth2Linkedin()); +Response::setModel(new OAuth2Disqus()); +Response::setModel(new OAuth2Amazon()); +Response::setModel(new OAuth2Etsy()); +Response::setModel(new OAuth2Facebook()); Response::setModel(new PolicyPasswordDictionary()); Response::setModel(new PolicyPasswordHistory()); Response::setModel(new PolicyPasswordPersonalData()); diff --git a/src/Appwrite/Auth/OAuth2/Discord.php b/src/Appwrite/Auth/OAuth2/Discord.php index 6cb682479a..a5ecdb5e3c 100644 --- a/src/Appwrite/Auth/OAuth2/Discord.php +++ b/src/Appwrite/Auth/OAuth2/Discord.php @@ -1,7 +1,6 @@ addHeader('Accept', 'application/json'); - + $response = $client->fetch( url: 'https://github.com/login/oauth/access_token', method: FetchClient::METHOD_POST, @@ -233,19 +234,19 @@ class Github extends OAuth2 'client_secret' => $this->appSecret, 'code' => 'intentionally-invalid-code', 'redirect_uri' => 'intentionally-invalid-redirect', - ] + ] ); - + $json = \json_decode($response->getBody(), true); - + if (isset($json['error']) && $json['error'] === "Not Found") { throw new \Exception('GitHub application with provided Client ID is does not exist.'); } - + if (isset($json['error']) && $json['error'] === "incorrect_client_credentials") { throw new \Exception('GitHub application with provided Client ID is valid, but the provided Client Secret is incorrect.'); } - + // We still expect error, like redirect_uri_mismatch or bad_verification_code, // but that indicates valid credentials } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php new file mode 100644 index 0000000000..b17ce97930 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php @@ -0,0 +1,40 @@ +verifyCredentials(); } $oAuthProviders[$enabledKey] = true; - } catch(\Throwable $err) { - if($enabled === true) { + } catch (\Throwable $err) { + if ($enabled === true) { throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Could not enable OAuth2 provider: ' . $err->getMessage()); } } @@ -188,7 +188,7 @@ abstract class Base extends Action 'oAuthProviders' => $oAuthProviders ]); - $project = $authorization->skip(fn() => $dbForPlatform->updateDocument('projects', $project->getId(), $updates)); + $project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates)); $response->dynamic(new Document([ '$id' => $providerId, diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php new file mode 100644 index 0000000000..978b5c9323 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php @@ -0,0 +1,50 @@ +addAction(UpdateAuthMethod::getName(), new UpdateAuthMethod()); - + // OAuth2 $this->addAction(UpdateOAuth2GitHub::getName(), new UpdateOAuth2GitHub()); $this->addAction(UpdateOAuth2Discord::getName(), new UpdateOAuth2Discord()); @@ -151,5 +169,23 @@ class Http extends Service $this->addAction(UpdateOAuth2Box::getName(), new UpdateOAuth2Box()); $this->addAction(UpdateOAuth2Autodesk::getName(), new UpdateOAuth2Autodesk()); $this->addAction(UpdateOAuth2Google::getName(), new UpdateOAuth2Google()); + $this->addAction(UpdateOAuth2Zoom::getName(), new UpdateOAuth2Zoom()); + $this->addAction(UpdateOAuth2Zoho::getName(), new UpdateOAuth2Zoho()); + $this->addAction(UpdateOAuth2Yandex::getName(), new UpdateOAuth2Yandex()); + $this->addAction(UpdateOAuth2X::getName(), new UpdateOAuth2X()); + $this->addAction(UpdateOAuth2WordPress::getName(), new UpdateOAuth2WordPress()); + $this->addAction(UpdateOAuth2Twitch::getName(), new UpdateOAuth2Twitch()); + $this->addAction(UpdateOAuth2Stripe::getName(), new UpdateOAuth2Stripe()); + $this->addAction(UpdateOAuth2Spotify::getName(), new UpdateOAuth2Spotify()); + $this->addAction(UpdateOAuth2Slack::getName(), new UpdateOAuth2Slack()); + $this->addAction(UpdateOAuth2Podio::getName(), new UpdateOAuth2Podio()); + $this->addAction(UpdateOAuth2Notion::getName(), new UpdateOAuth2Notion()); + $this->addAction(UpdateOAuth2Salesforce::getName(), new UpdateOAuth2Salesforce()); + $this->addAction(UpdateOAuth2Yahoo::getName(), new UpdateOAuth2Yahoo()); + $this->addAction(UpdateOAuth2Linkedin::getName(), new UpdateOAuth2Linkedin()); + $this->addAction(UpdateOAuth2Disqus::getName(), new UpdateOAuth2Disqus()); + $this->addAction(UpdateOAuth2Amazon::getName(), new UpdateOAuth2Amazon()); + $this->addAction(UpdateOAuth2Etsy::getName(), new UpdateOAuth2Etsy()); + $this->addAction(UpdateOAuth2Facebook::getName(), new UpdateOAuth2Facebook()); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index dc315d83fd..d005872845 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -288,6 +288,24 @@ class Response extends SwooleResponse public const MODEL_OAUTH2_BOX = 'oAuth2Box'; public const MODEL_OAUTH2_AUTODESK = 'oAuth2Autodesk'; public const MODEL_OAUTH2_GOOGLE = 'oAuth2Google'; + public const MODEL_OAUTH2_ZOOM = 'oAuth2Zoom'; + public const MODEL_OAUTH2_ZOHO = 'oAuth2Zoho'; + public const MODEL_OAUTH2_YANDEX = 'oAuth2Yandex'; + public const MODEL_OAUTH2_X = 'oAuth2X'; + public const MODEL_OAUTH2_WORDPRESS = 'oAuth2WordPress'; + public const MODEL_OAUTH2_TWITCH = 'oAuth2Twitch'; + public const MODEL_OAUTH2_STRIPE = 'oAuth2Stripe'; + public const MODEL_OAUTH2_SPOTIFY = 'oAuth2Spotify'; + public const MODEL_OAUTH2_SLACK = 'oAuth2Slack'; + public const MODEL_OAUTH2_PODIO = 'oAuth2Podio'; + public const MODEL_OAUTH2_NOTION = 'oAuth2Notion'; + public const MODEL_OAUTH2_SALESFORCE = 'oAuth2Salesforce'; + public const MODEL_OAUTH2_YAHOO = 'oAuth2Yahoo'; + public const MODEL_OAUTH2_LINKEDIN = 'oAuth2Linkedin'; + public const MODEL_OAUTH2_DISQUS = 'oAuth2Disqus'; + public const MODEL_OAUTH2_AMAZON = 'oAuth2Amazon'; + public const MODEL_OAUTH2_ETSY = 'oAuth2Etsy'; + public const MODEL_OAUTH2_FACEBOOK = 'oAuth2Facebook'; // Health public const MODEL_HEALTH_STATUS = 'healthStatus'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Amazon.php b/src/Appwrite/Utopia/Response/Model/OAuth2Amazon.php new file mode 100644 index 0000000000..33708374cc --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Amazon.php @@ -0,0 +1,43 @@ + Date: Fri, 24 Apr 2026 14:23:04 +0200 Subject: [PATCH 11/51] Improve OAuth SDK quality --- .../Project/Http/Project/OAuth2/Amazon/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Autodesk/Update.php | 5 +++++ .../Modules/Project/Http/Project/OAuth2/Base.php | 9 ++++++++- .../Project/Http/Project/OAuth2/Bitbucket/Update.php | 5 +++++ .../Modules/Project/Http/Project/OAuth2/Bitly/Update.php | 5 +++++ .../Modules/Project/Http/Project/OAuth2/Box/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Dailymotion/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Discord/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Disqus/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Dropbox/Update.php | 5 +++++ .../Modules/Project/Http/Project/OAuth2/Etsy/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Facebook/Update.php | 5 +++++ .../Modules/Project/Http/Project/OAuth2/Figma/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/GitHub/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Google/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Linkedin/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Notion/Update.php | 5 +++++ .../Modules/Project/Http/Project/OAuth2/Podio/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Salesforce/Update.php | 5 +++++ .../Modules/Project/Http/Project/OAuth2/Slack/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Spotify/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Stripe/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Twitch/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/WordPress/Update.php | 5 +++++ .../Modules/Project/Http/Project/OAuth2/X/Update.php | 5 +++++ .../Modules/Project/Http/Project/OAuth2/Yahoo/Update.php | 5 +++++ .../Project/Http/Project/OAuth2/Yandex/Update.php | 5 +++++ .../Modules/Project/Http/Project/OAuth2/Zoho/Update.php | 5 +++++ .../Modules/Project/Http/Project/OAuth2/Zoom/Update.php | 5 +++++ 29 files changed, 148 insertions(+), 1 deletion(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php index b17ce97930..0129daf7f4 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Amazon'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Amazon'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_AMAZON; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php index 29eaacdc87..6d959479f6 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Autodesk'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Autodesk'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_AUTODESK; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php index 9e4d1d6a05..aaf1c1edc0 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php @@ -88,6 +88,13 @@ abstract class Base extends Action return 'clientSecret'; } + /** + * SDK method name exposed to clients. + * + * @return string e.g. 'updateOAuth2GitHub' + */ + abstract public static function getProviderSDKMethod(): string; + public static function getName() { return 'updateProjectOAuth2' . static::getProviderLabel(); @@ -110,7 +117,7 @@ abstract class Base extends Action ->label('sdk', new Method( namespace: 'project', group: 'oauth2', - name: 'updateOAuth2' . $providerLabel, + name: static::getProviderSDKMethod(), description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.', auth: [AuthType::ADMIN, AuthType::KEY], responses: [ diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php index 0cd4b0ea2f..bc430101e5 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Bitbucket'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Bitbucket'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_BITBUCKET; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php index 28f89c8891..9bb56ce221 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Bitly'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Bitly'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_BITLY; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php index 086930de20..306a7c8529 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Box'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Box'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_BOX; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php index 825683f3a2..2d4cb3307a 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Dailymotion'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Dailymotion'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_DAILYMOTION; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php index 383aee12d6..449ed1067f 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Discord'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Discord'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_DISCORD; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php index 978b5c9323..50902c0263 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Disqus'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Disqus'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_DISQUS; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php index 6cc34cc612..27d2444955 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Dropbox'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Dropbox'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_DROPBOX; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php index 71e5ad14a8..36d79d2c99 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Etsy'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Etsy'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_ETSY; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php index ae8015db33..9a435b6123 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Facebook'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Facebook'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_FACEBOOK; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php index c19b9fb30f..2fa62a8428 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Figma'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Figma'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_FIGMA; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php index 4490fa90cd..04c6af54ee 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'GitHub'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2GitHub'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_GITHUB; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php index 466f7df464..f8d2cc21a2 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Google'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Google'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_GOOGLE; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php index 97755e4b77..39ae950e03 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Linkedin'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Linkedin'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_LINKEDIN; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php index c32c54ece6..5c8473d75d 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Notion'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Notion'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_NOTION; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php index 1e82e41a6c..9ad95ecef2 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Podio'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Podio'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_PODIO; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php index 99973e71fb..be75dfa9f5 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Salesforce'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Salesforce'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_SALESFORCE; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php index 8a2e351326..589ecd16b3 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Slack'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Slack'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_SLACK; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php index 9b7335791d..58e54891e8 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Spotify'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Spotify'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_SPOTIFY; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php index 39e9d67716..beed3737be 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Stripe'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Stripe'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_STRIPE; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Twitch/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Twitch/Update.php index f9b9ede9e3..73e473d9a2 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Twitch/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Twitch/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Twitch'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Twitch'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_TWITCH; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/WordPress/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/WordPress/Update.php index ab5a82c49a..a7f744cfe5 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/WordPress/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/WordPress/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'WordPress'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2WordPress'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_WORDPRESS; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/X/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/X/Update.php index 583d31209d..a232fe8f28 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/X/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/X/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'X'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2X'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_X; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yahoo/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yahoo/Update.php index 4097847e82..9160954e9c 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yahoo/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yahoo/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Yahoo'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Yahoo'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_YAHOO; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yandex/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yandex/Update.php index bda2b75523..15a03252a3 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yandex/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yandex/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Yandex'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Yandex'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_YANDEX; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoho/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoho/Update.php index 843a29bd9c..a0a88cbeed 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoho/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoho/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Zoho'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Zoho'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_ZOHO; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoom/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoom/Update.php index f48e3bc3d7..8cc99f4e03 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoom/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoom/Update.php @@ -23,6 +23,11 @@ class Update extends Base return 'Zoom'; } + public static function getProviderSDKMethod(): string + { + return 'updateOAuth2Zoom'; + } + public static function getResponseModel(): string { return Response::MODEL_OAUTH2_ZOOM; From 975da667f5a8ad503139f2805e1d50acdeb3bd74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 24 Apr 2026 14:23:19 +0200 Subject: [PATCH 12/51] Remove leftover --- analyze.sh | 75 ------------------------------------------------------ 1 file changed, 75 deletions(-) delete mode 100755 analyze.sh diff --git a/analyze.sh b/analyze.sh deleted file mode 100755 index 1620e9bb73..0000000000 --- a/analyze.sh +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -ROOT="/Users/matejbaco/Documents/GitHub/appwrite" -ENDPOINT_DIR="$ROOT/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2" -CONFIG_FILE="$ROOT/app/config/oAuthProviders.php" - -if ! command -v php >/dev/null 2>&1; then - echo "php is required but was not found in PATH" >&2 - exit 1 -fi - -if [[ ! -d "$ENDPOINT_DIR" ]]; then - echo "Endpoint directory not found: $ENDPOINT_DIR" >&2 - exit 1 -fi - -if [[ ! -f "$CONFIG_FILE" ]]; then - echo "Config file not found: $CONFIG_FILE" >&2 - exit 1 -fi - -echo "OAuth2 endpoint files:" -find "$ENDPOINT_DIR" -type f | sort -echo - -tmp_dir="$(mktemp -d)" -trap 'rm -rf "$tmp_dir"' EXIT - -endpoint_file="$tmp_dir/endpoint-providers.txt" -config_file="$tmp_dir/config-providers.txt" - -find "$ENDPOINT_DIR" -mindepth 2 -maxdepth 2 -type f -name 'Update.php' \ - | while read -r file; do - basename "$(dirname "$file")" | tr '[:upper:]' '[:lower:]' - done \ - | sort -u > "$endpoint_file" - -php -r ' - $providers = require $argv[1]; - $names = []; - - foreach ($providers as $provider) { - if (($provider["mock"] ?? false) === true) { - continue; - } - - $class = $provider["class"] ?? ""; - if ($class === "") { - continue; - } - - $base = substr($class, strrpos($class, "\\") + 1); - $names[strtolower($base)] = true; - } - - $names = array_keys($names); - sort($names); - - foreach ($names as $name) { - echo $name, PHP_EOL; - } -' "$CONFIG_FILE" > "$config_file" - -echo "Configured provider classes:" -cat "$config_file" -echo - -echo "Endpoint provider directories:" -cat "$endpoint_file" -echo - -echo "Configured providers without endpoint:" -comm -23 "$config_file" "$endpoint_file" From a62ca8612d96da5e45363bc4430b0a29e0922899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 24 Apr 2026 14:31:38 +0200 Subject: [PATCH 13/51] More OAuth endpoints --- app/init/models.php | 8 +++ .../Http/Project/OAuth2/Paypal/Update.php | 50 +++++++++++++++++ .../Project/OAuth2/PaypalSandbox/Update.php | 50 +++++++++++++++++ .../Http/Project/OAuth2/Tradeshift/Update.php | 55 +++++++++++++++++++ .../Project/OAuth2/TradeshiftBox/Update.php | 55 +++++++++++++++++++ .../Modules/Project/Services/Http.php | 8 +++ src/Appwrite/Utopia/Response.php | 4 ++ .../Utopia/Response/Model/OAuth2Paypal.php | 53 ++++++++++++++++++ .../Response/Model/OAuth2PaypalSandbox.php | 53 ++++++++++++++++++ .../Response/Model/OAuth2Tradeshift.php | 53 ++++++++++++++++++ .../Response/Model/OAuth2TradeshiftBox.php | 53 ++++++++++++++++++ 11 files changed, 442 insertions(+) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/PaypalSandbox/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Tradeshift/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/TradeshiftBox/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Paypal.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2PaypalSandbox.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Tradeshift.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2TradeshiftBox.php diff --git a/app/init/models.php b/app/init/models.php index df0d0d28d8..b5cd534133 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -121,11 +121,15 @@ use Appwrite\Utopia\Response\Model\OAuth2GitHub; use Appwrite\Utopia\Response\Model\OAuth2Google; use Appwrite\Utopia\Response\Model\OAuth2Linkedin; use Appwrite\Utopia\Response\Model\OAuth2Notion; +use Appwrite\Utopia\Response\Model\OAuth2Paypal; +use Appwrite\Utopia\Response\Model\OAuth2PaypalSandbox; use Appwrite\Utopia\Response\Model\OAuth2Podio; use Appwrite\Utopia\Response\Model\OAuth2Salesforce; use Appwrite\Utopia\Response\Model\OAuth2Slack; use Appwrite\Utopia\Response\Model\OAuth2Spotify; use Appwrite\Utopia\Response\Model\OAuth2Stripe; +use Appwrite\Utopia\Response\Model\OAuth2Tradeshift; +use Appwrite\Utopia\Response\Model\OAuth2TradeshiftBox; use Appwrite\Utopia\Response\Model\OAuth2Twitch; use Appwrite\Utopia\Response\Model\OAuth2WordPress; use Appwrite\Utopia\Response\Model\OAuth2X; @@ -406,6 +410,10 @@ Response::setModel(new OAuth2Disqus()); Response::setModel(new OAuth2Amazon()); Response::setModel(new OAuth2Etsy()); Response::setModel(new OAuth2Facebook()); +Response::setModel(new OAuth2Tradeshift()); +Response::setModel(new OAuth2TradeshiftBox()); +Response::setModel(new OAuth2Paypal()); +Response::setModel(new OAuth2PaypalSandbox()); Response::setModel(new PolicyPasswordDictionary()); Response::setModel(new PolicyPasswordHistory()); Response::setModel(new PolicyPasswordPersonalData()); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php new file mode 100644 index 0000000000..a223de70f5 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php @@ -0,0 +1,50 @@ +addAction(UpdateOAuth2Amazon::getName(), new UpdateOAuth2Amazon()); $this->addAction(UpdateOAuth2Etsy::getName(), new UpdateOAuth2Etsy()); $this->addAction(UpdateOAuth2Facebook::getName(), new UpdateOAuth2Facebook()); + $this->addAction(UpdateOAuth2Tradeshift::getName(), new UpdateOAuth2Tradeshift()); + $this->addAction(UpdateOAuth2TradeshiftBox::getName(), new UpdateOAuth2TradeshiftBox()); + $this->addAction(UpdateOAuth2Paypal::getName(), new UpdateOAuth2Paypal()); + $this->addAction(UpdateOAuth2PaypalSandbox::getName(), new UpdateOAuth2PaypalSandbox()); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index d005872845..85780e3b5c 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -306,6 +306,10 @@ class Response extends SwooleResponse public const MODEL_OAUTH2_AMAZON = 'oAuth2Amazon'; public const MODEL_OAUTH2_ETSY = 'oAuth2Etsy'; public const MODEL_OAUTH2_FACEBOOK = 'oAuth2Facebook'; + public const MODEL_OAUTH2_TRADESHIFT = 'oAuth2Tradeshift'; + public const MODEL_OAUTH2_TRADESHIFT_BOX = 'oAuth2TradeshiftBox'; + public const MODEL_OAUTH2_PAYPAL = 'oAuth2Paypal'; + public const MODEL_OAUTH2_PAYPAL_SANDBOX = 'oAuth2PaypalSandbox'; // Health public const MODEL_HEALTH_STATUS = 'healthStatus'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Paypal.php b/src/Appwrite/Utopia/Response/Model/OAuth2Paypal.php new file mode 100644 index 0000000000..b8e836eedd --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Paypal.php @@ -0,0 +1,53 @@ + Date: Fri, 24 Apr 2026 15:02:36 +0200 Subject: [PATCH 14/51] More OAuth endpoints --- app/init/models.php | 6 + .../Http/Project/OAuth2/Auth0/Update.php | 145 ++++++++++++++++ .../Http/Project/OAuth2/Authentik/Update.php | 142 ++++++++++++++++ .../Project/Http/Project/OAuth2/Base.php | 45 +++-- .../Http/Project/OAuth2/Gitlab/Update.php | 156 ++++++++++++++++++ .../Modules/Project/Services/Http.php | 6 + src/Appwrite/Utopia/Response.php | 3 + .../Utopia/Response/Model/OAuth2Auth0.php | 55 ++++++ .../Utopia/Response/Model/OAuth2Authentik.php | 55 ++++++ .../Utopia/Response/Model/OAuth2Gitlab.php | 75 +++++++++ 10 files changed, 677 insertions(+), 11 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Auth0.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Authentik.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Gitlab.php diff --git a/app/init/models.php b/app/init/models.php index b5cd534133..0ccff38a23 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -106,6 +106,8 @@ use Appwrite\Utopia\Response\Model\Mock; use Appwrite\Utopia\Response\Model\MockNumber; use Appwrite\Utopia\Response\Model\None; use Appwrite\Utopia\Response\Model\OAuth2Amazon; +use Appwrite\Utopia\Response\Model\OAuth2Auth0; +use Appwrite\Utopia\Response\Model\OAuth2Authentik; use Appwrite\Utopia\Response\Model\OAuth2Autodesk; use Appwrite\Utopia\Response\Model\OAuth2Bitbucket; use Appwrite\Utopia\Response\Model\OAuth2Bitly; @@ -118,6 +120,7 @@ use Appwrite\Utopia\Response\Model\OAuth2Etsy; use Appwrite\Utopia\Response\Model\OAuth2Facebook; 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\OAuth2Linkedin; use Appwrite\Utopia\Response\Model\OAuth2Notion; @@ -414,6 +417,9 @@ Response::setModel(new OAuth2Tradeshift()); Response::setModel(new OAuth2TradeshiftBox()); Response::setModel(new OAuth2Paypal()); Response::setModel(new OAuth2PaypalSandbox()); +Response::setModel(new OAuth2Gitlab()); +Response::setModel(new OAuth2Authentik()); +Response::setModel(new OAuth2Auth0()); Response::setModel(new PolicyPasswordDictionary()); Response::setModel(new PolicyPasswordHistory()); Response::setModel(new PolicyPasswordPersonalData()); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php new file mode 100644 index 0000000000..d551689d82 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php @@ -0,0 +1,145 @@ +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: static::getProviderSDKMethod(), + description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: static::getResponseModel(), + ) + ], + )) + ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) + ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) + ->param('endpoint', null, new Nullable(new Text(256, 0)), 'Domain of Auth0 instance. For example: example.us.auth0.com', 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->handle(...)); + } + + /** + * Custom callback used instead of the parent's `action()` because Auth0 + * takes an additional optional `endpoint` parameter. The method is named + * differently to avoid an LSP-incompatible override of Base::action(). + */ + public function handle( + ?string $clientId, + ?string $clientSecret, + ?string $endpoint, + ?bool $enabled, + Response $response, + Database $dbForPlatform, + Document $project, + Authorization $authorization + ): void { + $providerId = static::getProviderId(); + + // The secret is stored as JSON `{"clientSecret": "...", "auth0Domain": "..."}` + // to match the shape Auth0's OAuth2 adapter expects (getAuth0Domain()). + // Merge new values with existing storage so that submitting only one of + // `clientSecret`/`endpoint` leaves the other untouched. + $encodedSecret = null; + if (!\is_null($clientSecret) || !\is_null($endpoint)) { + $storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? ''; + $existing = []; + if (!empty($storedRaw)) { + $existing = \json_decode($storedRaw, true) ?: []; + } + $encodedSecret = \json_encode([ + 'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''), + 'auth0Domain' => $endpoint ?? ($existing['auth0Domain'] ?? ''), + ]); + } + + $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled); + + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; + $decoded = []; + if (!empty($storedRaw)) { + $decoded = \json_decode($storedRaw, true) ?: []; + } + + $response->dynamic(new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => $decoded['clientSecret'] ?? '', + 'endpoint' => $decoded['auth0Domain'] ?? '', + ]), static::getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php new file mode 100644 index 0000000000..2b69319a71 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php @@ -0,0 +1,142 @@ +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: static::getProviderSDKMethod(), + description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: static::getResponseModel(), + ) + ], + )) + ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) + ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) + ->param('endpoint', '', new Text(256, 1), 'Domain of Authentik instance. For example: example.authentik.com', optional: false) + ->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->handle(...)); + } + + /** + * Custom callback used instead of the parent's `action()` because Authentik + * takes an additional required `endpoint` parameter. The method is named + * differently to avoid an LSP-incompatible override of Base::action(). + */ + public function handle( + ?string $clientId, + ?string $clientSecret, + string $endpoint, + ?bool $enabled, + Response $response, + Database $dbForPlatform, + Document $project, + Authorization $authorization + ): void { + $providerId = static::getProviderId(); + + // The secret is stored as JSON `{"clientSecret": "...", "authentikDomain": "..."}` + // to match the shape Authentik's OAuth2 adapter expects (getAuthentikDomain()). + // The `endpoint` param is required on every call, so it's always written. + // `clientSecret` is optional; if omitted, the existing stored secret is preserved. + $storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? ''; + $existing = []; + if (!empty($storedRaw)) { + $existing = \json_decode($storedRaw, true) ?: []; + } + $encodedSecret = \json_encode([ + 'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''), + 'authentikDomain' => $endpoint, + ]); + + $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled); + + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; + $decoded = []; + if (!empty($storedRaw)) { + $decoded = \json_decode($storedRaw, true) ?: []; + } + + $response->dynamic(new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => $decoded['clientSecret'] ?? '', + 'endpoint' => $decoded['authentikDomain'] ?? '', + ]), static::getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php index aaf1c1edc0..2d74c1b61d 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php @@ -137,15 +137,23 @@ abstract class Base extends Action ->callback($this->action(...)); } - public function action( + /** + * Apply the provided credential changes to the project's oAuthProviders map, + * run the optional credential verification hook, persist the project, and + * return the updated project document. + * + * Providers that need to serialize multiple values into a single secret + * (e.g. GitLab, which stores `{clientSecret, endpoint}` as JSON) should + * encode those values into `$clientSecret` before calling this method. + */ + protected function persistCredentials( + Document $project, + Database $dbForPlatform, + Authorization $authorization, ?string $clientId, ?string $clientSecret, - ?bool $enabled, - Response $response, - Database $dbForPlatform, - Document $project, - Authorization $authorization - ): void { + ?bool $enabled + ): Document { $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.'); @@ -195,13 +203,28 @@ abstract class Base extends Action 'oAuthProviders' => $oAuthProviders ]); - $project = $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates)); + return $authorization->skip(fn () => $dbForPlatform->updateDocument('projects', $project->getId(), $updates)); + } + + public function action( + ?string $clientId, + ?string $clientSecret, + ?bool $enabled, + Response $response, + Database $dbForPlatform, + Document $project, + Authorization $authorization + ): void { + $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $clientSecret, $enabled); + + $providerId = static::getProviderId(); + $oAuthProviders = $project->getAttribute('oAuthProviders', []); $response->dynamic(new Document([ '$id' => $providerId, - 'enabled' => $oAuthProviders[$enabledKey] ?? false, - static::getClientIdParamName() => $oAuthProviders[$appIdKey] ?? '', - static::getClientSecretParamName() => $oAuthProviders[$appSecretKey] ?? '', + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => $oAuthProviders[$providerId . 'Secret'] ?? '', ]), static::getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php new file mode 100644 index 0000000000..fafc97c836 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php @@ -0,0 +1,156 @@ +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: static::getProviderSDKMethod(), + description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: static::getResponseModel(), + ) + ], + )) + ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) + ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) + ->param('endpoint', null, new Nullable(new URL()), 'Endpoint URL of self-hosted GitLab instance. For example: https://gitlab.com', 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->handle(...)); + } + + /** + * Custom callback used instead of the parent's `action()` because Gitlab + * takes an additional `endpoint` parameter. The method is named + * differently to avoid an LSP-incompatible override of Base::action(). + */ + public function handle( + ?string $applicationId, + ?string $secret, + ?string $endpoint, + ?bool $enabled, + Response $response, + Database $dbForPlatform, + Document $project, + Authorization $authorization + ): void { + $providerId = static::getProviderId(); + + // The secret is stored as JSON `{"clientSecret": "...", "endpoint": "..."}` + // so that the Gitlab OAuth2 adapter can extract the endpoint via getEndpoint(). + // Merge the new values with what's already stored so that submitting only + // one of `secret`/`endpoint` leaves the other untouched. + $encodedSecret = null; + if (!\is_null($secret) || !\is_null($endpoint)) { + $storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? ''; + $existing = []; + if (!empty($storedRaw)) { + $existing = \json_decode($storedRaw, true) ?: []; + } + $encodedSecret = \json_encode([ + 'clientSecret' => $secret ?? ($existing['clientSecret'] ?? ''), + 'endpoint' => $endpoint ?? ($existing['endpoint'] ?? ''), + ]); + } + + $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $applicationId, $encodedSecret, $enabled); + + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; + $decoded = []; + if (!empty($storedRaw)) { + $decoded = \json_decode($storedRaw, true) ?: []; + } + + $response->dynamic(new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => $decoded['clientSecret'] ?? '', + 'endpoint' => $decoded['endpoint'] ?? '', + ]), static::getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index a5fd19c6b0..47a48c331d 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -17,6 +17,8 @@ use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\Get as GetMockPhone use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\Update as UpdateMockPhone; use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\XList as ListMockPhones; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Amazon\Update as UpdateOAuth2Amazon; +use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Auth0\Update as UpdateOAuth2Auth0; +use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Authentik\Update as UpdateOAuth2Authentik; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Autodesk\Update as UpdateOAuth2Autodesk; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Bitbucket\Update as UpdateOAuth2Bitbucket; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Bitly\Update as UpdateOAuth2Bitly; @@ -29,6 +31,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Etsy\Update as UpdateO use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Facebook\Update as UpdateOAuth2Facebook; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Figma\Update as UpdateOAuth2Figma; 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\Linkedin\Update as UpdateOAuth2Linkedin; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Notion\Update as UpdateOAuth2Notion; @@ -195,5 +198,8 @@ class Http extends Service $this->addAction(UpdateOAuth2TradeshiftBox::getName(), new UpdateOAuth2TradeshiftBox()); $this->addAction(UpdateOAuth2Paypal::getName(), new UpdateOAuth2Paypal()); $this->addAction(UpdateOAuth2PaypalSandbox::getName(), new UpdateOAuth2PaypalSandbox()); + $this->addAction(UpdateOAuth2Gitlab::getName(), new UpdateOAuth2Gitlab()); + $this->addAction(UpdateOAuth2Authentik::getName(), new UpdateOAuth2Authentik()); + $this->addAction(UpdateOAuth2Auth0::getName(), new UpdateOAuth2Auth0()); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 85780e3b5c..099d42ec25 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -310,6 +310,9 @@ class Response extends SwooleResponse public const MODEL_OAUTH2_TRADESHIFT_BOX = 'oAuth2TradeshiftBox'; public const MODEL_OAUTH2_PAYPAL = 'oAuth2Paypal'; public const MODEL_OAUTH2_PAYPAL_SANDBOX = 'oAuth2PaypalSandbox'; + public const MODEL_OAUTH2_GITLAB = 'oAuth2Gitlab'; + public const MODEL_OAUTH2_AUTHENTIK = 'oAuth2Authentik'; + public const MODEL_OAUTH2_AUTH0 = 'oAuth2Auth0'; // Health public const MODEL_HEALTH_STATUS = 'healthStatus'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Auth0.php b/src/Appwrite/Utopia/Response/Model/OAuth2Auth0.php new file mode 100644 index 0000000000..89cf1c92d5 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Auth0.php @@ -0,0 +1,55 @@ +addRule('endpoint', [ + 'type' => self::TYPE_STRING, + 'description' => 'Auth0 OAuth 2 endpoint domain.', + 'default' => '', + 'example' => 'example.us.auth0.com', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'OAuth2Auth0'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_OAUTH2_AUTH0; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Authentik.php b/src/Appwrite/Utopia/Response/Model/OAuth2Authentik.php new file mode 100644 index 0000000000..ca6e828ed4 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Authentik.php @@ -0,0 +1,55 @@ +addRule('endpoint', [ + 'type' => self::TYPE_STRING, + 'description' => 'Authentik OAuth 2 endpoint domain.', + 'default' => '', + 'example' => 'example.authentik.com', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'OAuth2Authentik'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_OAUTH2_AUTHENTIK; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Gitlab.php b/src/Appwrite/Utopia/Response/Model/OAuth2Gitlab.php new file mode 100644 index 0000000000..bae60c2f5d --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Gitlab.php @@ -0,0 +1,75 @@ +addRule('endpoint', [ + 'type' => self::TYPE_STRING, + 'description' => 'GitLab OAuth 2 endpoint URL. Defaults to https://gitlab.com for self-hosted instances.', + 'default' => '', + 'example' => 'https://gitlab.com', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'OAuth2Gitlab'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_OAUTH2_GITLAB; + } +} From d9d87f813fac754648a5503fc1ea2342392a0f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 24 Apr 2026 16:31:21 +0200 Subject: [PATCH 15/51] apple oauth endpoints --- app/init/models.php | 2 + .../Http/Project/OAuth2/Apple/Update.php | 158 ++++++++++++++++++ src/Appwrite/Utopia/Response.php | 1 + .../Utopia/Response/Model/OAuth2Apple.php | 93 +++++++++++ 4 files changed, 254 insertions(+) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Apple.php diff --git a/app/init/models.php b/app/init/models.php index 0ccff38a23..e515713914 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -106,6 +106,7 @@ use Appwrite\Utopia\Response\Model\Mock; use Appwrite\Utopia\Response\Model\MockNumber; use Appwrite\Utopia\Response\Model\None; use Appwrite\Utopia\Response\Model\OAuth2Amazon; +use Appwrite\Utopia\Response\Model\OAuth2Apple; use Appwrite\Utopia\Response\Model\OAuth2Auth0; use Appwrite\Utopia\Response\Model\OAuth2Authentik; use Appwrite\Utopia\Response\Model\OAuth2Autodesk; @@ -420,6 +421,7 @@ Response::setModel(new OAuth2PaypalSandbox()); Response::setModel(new OAuth2Gitlab()); Response::setModel(new OAuth2Authentik()); Response::setModel(new OAuth2Auth0()); +Response::setModel(new OAuth2Apple()); Response::setModel(new PolicyPasswordDictionary()); Response::setModel(new PolicyPasswordHistory()); Response::setModel(new PolicyPasswordPersonalData()); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php new file mode 100644 index 0000000000..edbfdb8b9e --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php @@ -0,0 +1,158 @@ +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: static::getProviderSDKMethod(), + description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: static::getResponseModel(), + ) + ], + )) + ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) + ->param('keyId', null, new Nullable(new Text(256, 0)), 'Key ID of Apple OAuth2 app. For example: P4000000N8', optional: true) + ->param('teamId', null, new Nullable(new Text(256, 0)), 'Team ID of Apple OAuth2 app. For example: D4000000R6', optional: true) + ->param('p8File', null, new Nullable(new Text(4096, 0)), 'Contents of the Apple OAuth2 app .p8 private key file. The secret key wrapped by the PEM markers is 200 characters long. For example: -----BEGIN PRIVATE KEY-----MIGTAg...jy2Xbna-----END PRIVATE KEY-----', 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->handle(...)); + } + + /** + * Custom callback used instead of the parent's `action()` because Apple's + * client secret is composed of three fields (.p8 file contents, Key ID and + * Team ID) that must be JSON-encoded to match the shape Apple's OAuth2 + * adapter expects in getAppSecret(). The method is named differently to + * avoid an LSP-incompatible override of Base::action(). + */ + public function handle( + ?string $serviceId, + ?string $keyId, + ?string $teamId, + ?string $p8File, + ?bool $enabled, + Response $response, + Database $dbForPlatform, + Document $project, + Authorization $authorization + ): void { + $providerId = static::getProviderId(); + + // The secret is stored as JSON `{"p8": "...", "keyID": "...", "teamID": "..."}` + // to match the shape Apple's OAuth2 adapter expects in getAppSecret(). + // Merge new values with what's already stored so that submitting only + // some of the fields leaves the rest untouched. + $encodedSecret = null; + if (!\is_null($keyId) || !\is_null($teamId) || !\is_null($p8File)) { + $storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? ''; + $existing = []; + if (!empty($storedRaw)) { + $existing = \json_decode($storedRaw, true) ?: []; + } + $encodedSecret = \json_encode([ + 'p8' => $p8File ?? ($existing['p8'] ?? ''), + 'keyID' => $keyId ?? ($existing['keyID'] ?? ''), + 'teamID' => $teamId ?? ($existing['teamID'] ?? ''), + ]); + } + + $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $serviceId, $encodedSecret, $enabled); + + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; + $decoded = []; + if (!empty($storedRaw)) { + $decoded = \json_decode($storedRaw, true) ?: []; + } + + $response->dynamic(new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + 'keyId' => $decoded['keyID'] ?? '', + 'teamId' => $decoded['teamID'] ?? '', + 'p8File' => $decoded['p8'] ?? '', + ]), static::getResponseModel()); + } +} diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 099d42ec25..d929b3f98a 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -313,6 +313,7 @@ class Response extends SwooleResponse public const MODEL_OAUTH2_GITLAB = 'oAuth2Gitlab'; public const MODEL_OAUTH2_AUTHENTIK = 'oAuth2Authentik'; public const MODEL_OAUTH2_AUTH0 = 'oAuth2Auth0'; + public const MODEL_OAUTH2_APPLE = 'oAuth2Apple'; // Health public const MODEL_HEALTH_STATUS = 'healthStatus'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Apple.php b/src/Appwrite/Utopia/Response/Model/OAuth2Apple.php new file mode 100644 index 0000000000..8120090420 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Apple.php @@ -0,0 +1,93 @@ +addRule('enabled', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'OAuth 2 provider is active and can be used to create sessions.', + 'default' => false, + 'example' => false, + ]) + ->addRule($this->getClientIdFieldName(), [ + 'type' => self::TYPE_STRING, + 'description' => $this->getClientIdDescription(), + 'default' => '', + 'example' => $this->getClientIdExample(), + ]) + ->addRule('keyId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Apple OAuth 2 key ID.', + 'default' => '', + 'example' => 'P4000000N8', + ]) + ->addRule('teamId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Apple OAuth 2 team ID.', + 'default' => '', + 'example' => 'D4000000R6', + ]) + ->addRule('p8File', [ + 'type' => self::TYPE_STRING, + 'description' => 'Apple OAuth 2 .p8 private key file contents. The secret key wrapped by the PEM markers is 200 characters long.', + 'default' => '', + 'example' => '-----BEGIN PRIVATE KEY-----MIGTAg...jy2Xbna-----END PRIVATE KEY-----', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'OAuth2Apple'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_OAUTH2_APPLE; + } +} From 8200d079c621433422775b03873f8b8b1e4f97b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 24 Apr 2026 16:37:27 +0200 Subject: [PATCH 16/51] Simplify specs --- app/controllers/api/projects.php | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index cf920b695f..494aa11150 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -58,23 +58,11 @@ Http::get('/v1/projects/:projectId') $response->dynamic($project, Response::MODEL_PROJECT); }); +// Backwards compatibility Http::patch('/v1/projects/:projectId/oauth2') ->desc('Update project OAuth2') ->groups(['api', 'projects']) ->label('scope', 'projects.write') - ->label('sdk', new Method( - namespace: 'projects', - group: 'auth', - name: 'updateOAuth2', - description: '/docs/references/projects/update-oauth2.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_OK, - model: Response::MODEL_PROJECT, - ) - ] - )) ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) ->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'Provider Name') ->param('appId', null, new Nullable(new Text(256)), 'Provider app ID. Max length: 256 chars.', true) From ffd0dbd406ba84c2fc99b8f93472daa3a2bf098c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 25 Apr 2026 10:20:00 +0200 Subject: [PATCH 17/51] Add OIDC endpoint --- app/init/models.php | 2 + .../Http/Project/OAuth2/Oidc/Update.php | 182 ++++++++++++++++++ .../Modules/Project/Services/Http.php | 2 + src/Appwrite/Utopia/Response.php | 1 + .../Utopia/Response/Model/OAuth2Oidc.php | 74 +++++++ 5 files changed, 261 insertions(+) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php diff --git a/app/init/models.php b/app/init/models.php index e515713914..8d95e50d02 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -125,6 +125,7 @@ use Appwrite\Utopia\Response\Model\OAuth2Gitlab; use Appwrite\Utopia\Response\Model\OAuth2Google; use Appwrite\Utopia\Response\Model\OAuth2Linkedin; use Appwrite\Utopia\Response\Model\OAuth2Notion; +use Appwrite\Utopia\Response\Model\OAuth2Oidc; use Appwrite\Utopia\Response\Model\OAuth2Paypal; use Appwrite\Utopia\Response\Model\OAuth2PaypalSandbox; use Appwrite\Utopia\Response\Model\OAuth2Podio; @@ -421,6 +422,7 @@ Response::setModel(new OAuth2PaypalSandbox()); Response::setModel(new OAuth2Gitlab()); Response::setModel(new OAuth2Authentik()); Response::setModel(new OAuth2Auth0()); +Response::setModel(new OAuth2Oidc()); Response::setModel(new OAuth2Apple()); Response::setModel(new PolicyPasswordDictionary()); Response::setModel(new PolicyPasswordHistory()); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php new file mode 100644 index 0000000000..d8f85bd6b6 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php @@ -0,0 +1,182 @@ +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: static::getProviderSDKMethod(), + description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: static::getResponseModel(), + ) + ], + )) + ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) + ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) + ->param('wellKnownURL', null, new Nullable(new URL()), 'OpenID Connect well-known configuration URL. When provided, authorization, token, and user info endpoints can be discovered automatically. For example: https://myoauth.com/.well-known/openid-configuration', optional: true) + ->param('authorizationURL', null, new Nullable(new URL()), 'OpenID Connect authorization endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/authorize', optional: true) + ->param('tokenUrl', null, new Nullable(new URL()), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true) + ->param('userInfoUrl', null, new Nullable(new URL()), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', 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->handle(...)); + } + + /** + * Custom callback used instead of the parent's `action()` because OIDC takes + * a well-known URL plus three discovery URLs (authorization, token, user + * info), all stored together with the client secret as JSON. The method is + * named differently to avoid an LSP-incompatible override of Base::action(). + * + * Enabling the provider requires either a non-empty `wellKnownEndpoint`, + * or all three of `authorizationEndpoint`, `tokenEndpoint`, and + * `userInfoEndpoint` to be set. The check considers the merged state of + * existing stored values plus the new values from the request, so callers + * can enable the provider in a single request without re-sending fields + * that were configured previously. + */ + public function handle( + ?string $clientId, + ?string $clientSecret, + ?string $wellKnownURL, + ?string $authorizationURL, + ?string $tokenUrl, + ?string $userInfoUrl, + ?bool $enabled, + Response $response, + Database $dbForPlatform, + Document $project, + Authorization $authorization + ): void { + $providerId = static::getProviderId(); + + // The secret is stored as JSON + // `{"clientSecret": "...", "wellKnownEndpoint": "...", "authorizationEndpoint": "...", "tokenEndpoint": "...", "userInfoEndpoint": "..."}` + // so that the OIDC OAuth2 adapter can extract each endpoint individually. + // Merge new values with what's already stored so that submitting only a + // subset of fields leaves the others untouched. + $storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? ''; + $existing = []; + if (!empty($storedRaw)) { + $existing = \json_decode($storedRaw, true) ?: []; + } + + $merged = [ + 'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''), + 'wellKnownEndpoint' => $wellKnownURL ?? ($existing['wellKnownEndpoint'] ?? ''), + 'authorizationEndpoint' => $authorizationURL ?? ($existing['authorizationEndpoint'] ?? ''), + 'tokenEndpoint' => $tokenUrl ?? ($existing['tokenEndpoint'] ?? ''), + 'userInfoEndpoint' => $userInfoUrl ?? ($existing['userInfoEndpoint'] ?? ''), + ]; + + // When enabling, require either wellKnownEndpoint alone, or all three + // discovery URLs (authorization, token, user info). Skip this check + // when disabling or when leaving the enabled flag unchanged. + if ($enabled === true) { + $hasWellKnown = !empty($merged['wellKnownEndpoint']); + $hasAllDiscovery = !empty($merged['authorizationEndpoint']) + && !empty($merged['tokenEndpoint']) + && !empty($merged['userInfoEndpoint']); + + if (!$hasWellKnown && !$hasAllDiscovery) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Enabling OpenID Connect requires either wellKnownURL, or all of authorizationURL, tokenUrl, and userInfoUrl.'); + } + } + + $encodedSecret = \json_encode($merged); + + $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled); + + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; + $decoded = []; + if (!empty($storedRaw)) { + $decoded = \json_decode($storedRaw, true) ?: []; + } + + $response->dynamic(new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => $decoded['clientSecret'] ?? '', + 'wellKnownURL' => $decoded['wellKnownEndpoint'] ?? '', + 'authorizationURL' => $decoded['authorizationEndpoint'] ?? '', + 'tokenUrl' => $decoded['tokenEndpoint'] ?? '', + 'userInfoUrl' => $decoded['userInfoEndpoint'] ?? '', + ]), static::getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index 47a48c331d..c87e16107d 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -35,6 +35,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Gitlab\Update as Updat use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Google\Update as UpdateOAuth2Google; 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; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Paypal\Update as UpdateOAuth2Paypal; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\PaypalSandbox\Update as UpdateOAuth2PaypalSandbox; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Podio\Update as UpdateOAuth2Podio; @@ -201,5 +202,6 @@ class Http extends Service $this->addAction(UpdateOAuth2Gitlab::getName(), new UpdateOAuth2Gitlab()); $this->addAction(UpdateOAuth2Authentik::getName(), new UpdateOAuth2Authentik()); $this->addAction(UpdateOAuth2Auth0::getName(), new UpdateOAuth2Auth0()); + $this->addAction(UpdateOAuth2Oidc::getName(), new UpdateOAuth2Oidc()); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index d929b3f98a..190b16b4a0 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -313,6 +313,7 @@ class Response extends SwooleResponse public const MODEL_OAUTH2_GITLAB = 'oAuth2Gitlab'; public const MODEL_OAUTH2_AUTHENTIK = 'oAuth2Authentik'; public const MODEL_OAUTH2_AUTH0 = 'oAuth2Auth0'; + public const MODEL_OAUTH2_OIDC = 'oAuth2Oidc'; public const MODEL_OAUTH2_APPLE = 'oAuth2Apple'; // Health diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php b/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php new file mode 100644 index 0000000000..97a9ace5ad --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php @@ -0,0 +1,74 @@ +addRule('wellKnownURL', [ + 'type' => self::TYPE_STRING, + 'description' => 'OpenID Connect well-known configuration URL. When set, authorization, token, and user info endpoints can be discovered automatically.', + 'default' => '', + 'example' => 'https://myoauth.com/.well-known/openid-configuration', + ]) + ->addRule('authorizationURL', [ + 'type' => self::TYPE_STRING, + 'description' => 'OpenID Connect authorization endpoint URL.', + 'default' => '', + 'example' => 'https://myoauth.com/oauth2/authorize', + ]) + ->addRule('tokenUrl', [ + 'type' => self::TYPE_STRING, + 'description' => 'OpenID Connect token endpoint URL.', + 'default' => '', + 'example' => 'https://myoauth.com/oauth2/token', + ]) + ->addRule('userInfoUrl', [ + 'type' => self::TYPE_STRING, + 'description' => 'OpenID Connect user info endpoint URL.', + 'default' => '', + 'example' => 'https://myoauth.com/oauth2/userinfo', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'OAuth2Oidc'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_OAUTH2_OIDC; + } +} From a588a62277d90ac38351ac6bca3fcc07d7af8a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 25 Apr 2026 11:57:40 +0200 Subject: [PATCH 18/51] Prepare env for cicd integration with github oauth --- .env | 2 ++ .github/workflows/ci.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.env b/.env index 9abfa756e1..3dc7afe34a 100644 --- a/.env +++ b/.env @@ -146,3 +146,5 @@ _APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main _APP_TRUSTED_HEADERS=x-forwarded-for _APP_POOL_ADAPTER=stack _APP_WORKER_SCREENSHOTS_ROUTER=http://appwrite +_TESTS_OAUTH2_GITHUB_CLIENT_ID= +_TESTS_OAUTH2_GITHUB_CLIENT_SECRET= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a056ff8510..d28c00477a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -526,6 +526,8 @@ jobs: docker compose exec -T \ -e _APP_E2E_RESPONSE_FORMAT="${{ github.event.inputs.response_format }}" \ + -e _TESTS_OAUTH2_GITHUB_CLIENT_ID="${{ secrets.TESTS_OAUTH2_GITHUB_CLIENT_ID }}" \ + -e _TESTS_OAUTH2_GITHUB_CLIENT_SECRET="${{ secrets.TESTS_OAUTH2_GITHUB_CLIENT_SECRET }}" \ appwrite vendor/bin/paratest --processes "$PARATEST_PROCESSES" $FUNCTIONAL_FLAG "$SERVICE_PATH" --exclude-group abuseEnabled --exclude-group screenshots --log-junit tests/e2e/Services/${{ matrix.service }}/junit.xml - name: Failure Logs From 184399023c7f4beec33ada8db682b5b005685878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 25 Apr 2026 11:58:09 +0200 Subject: [PATCH 19/51] Add github integration test --- .../Project/OAuthGitHubIntegrationTest.php | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) create mode 100644 tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php diff --git a/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php b/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php new file mode 100644 index 0000000000..1a6f05ec6f --- /dev/null +++ b/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php @@ -0,0 +1,148 @@ +markTestSkipped('GitHub OAuth2 credentials not configured (_TESTS_OAUTH2_GITHUB_CLIENT_ID, _TESTS_OAUTH2_GITHUB_CLIENT_SECRET)'); + } + + $consoleHeaders = [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => 'console', + ]; + + // Step 1: Create new organization (team) + $team = $this->client->call(Client::METHOD_POST, '/teams', $consoleHeaders, [ + 'teamId' => ID::unique(), + 'name' => 'GitHub OAuth Org ' . uniqid(), + ]); + $this->assertSame(201, $team['headers']['status-code']); + $teamId = $team['body']['$id']; + + // Step 2: Create new project + $project = $this->client->call(Client::METHOD_POST, '/projects', $consoleHeaders, [ + 'projectId' => 'githuboauthapp', // Must be this ID, its used in redirect URL set in GitHub app configuration + 'name' => 'GitHub OAuth Project', + 'teamId' => $teamId, + 'region' => System::getEnv('_APP_REGION', 'default'), + ]); + $this->assertSame(201, $project['headers']['status-code']); + $newProjectId = $project['body']['$id']; + + // Step 3: Configure GitHub provider on the new project via PATCH /v1/project/oauth2/github + $newProjectAdminHeaders = [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + 'x-appwrite-project' => $newProjectId, + 'x-appwrite-mode' => 'admin', + ]; + + $configResponse = $this->client->call(Client::METHOD_PATCH, '/project/oauth2/github', $newProjectAdminHeaders, [ + 'clientId' => $clientId, + 'clientSecret' => $clientSecret, + 'enabled' => true, + ]); + $this->assertSame(200, $configResponse['headers']['status-code']); + $this->assertTrue($configResponse['body']['enabled']); + $this->assertSame($clientId, $configResponse['body']['clientId']); + + // Step 4: Verify OAuth provider is enabled via GET /v1/projects/:projectId + $projectDetails = $this->client->call(Client::METHOD_GET, '/projects/' . $newProjectId, $consoleHeaders); + $this->assertSame(200, $projectDetails['headers']['status-code']); + + $githubProvider = null; + foreach ($projectDetails['body']['oAuthProviders'] as $provider) { + if ($provider['key'] === 'github') { + $githubProvider = $provider; + break; + } + } + $this->assertNotNull($githubProvider, 'GitHub OAuth provider not found in project details'); + $this->assertTrue($githubProvider['enabled']); + $this->assertSame($clientId, $githubProvider['appId']); + $this->assertSame($clientSecret, $githubProvider['secret']); + + // Step 5: Without client headers (no API key), go through the OAuth flow + $clientHeaders = [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $newProjectId, + ]; + + $oauthInit = $this->client->call( + Client::METHOD_GET, + '/account/sessions/oauth2/github', + $clientHeaders, + [ + 'success' => 'http://localhost/v1/mock/tests/general/oauth2/success', + 'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure', + ], + followRedirects: false + ); + + $this->assertSame(301, $oauthInit['headers']['status-code']); + $this->assertArrayHasKey('location', $oauthInit['headers']); + $this->assertStringStartsWith('https://github.com/login/oauth/authorize', $oauthInit['headers']['location']); + $this->assertStringContainsString('client_id=' . \urlencode($clientId), $oauthInit['headers']['location']); + $this->assertStringContainsString('redirect_uri=', $oauthInit['headers']['location']); + + // Follow the redirect to GitHub's authorization endpoint. With a real user agent, GitHub + // would prompt for login + app approval, then redirect back to Appwrite's callback with a + // valid `code`. Appwrite would then exchange the code, create the session and redirect to + // the success URL with the session cookie set. + $oauthClient = new Client(); + $oauthClient->setEndpoint(''); + + $githubResponse = $oauthClient->call( + Client::METHOD_GET, + $oauthInit['headers']['location'], + [], + [], + decode: false, + followRedirects: false + ); + + // GitHub returns 200 (login HTML) or 302 (redirect to login) — both indicate the flow + // reached GitHub. Anything else means our redirect is malformed. + $this->assertContains($githubResponse['headers']['status-code'], [200, 302]); + + // Final step: GET /v1/account with the session cookie set by the OAuth callback. In an + // automated environment that completes the GitHub authorization step, the call below + // returns 200 with the OAuth user. Without that step (no GitHub login/approval automated + // here), there is no session cookie, so the call returns 401. + $sessionCookieName = 'a_session_' . $newProjectId; + $sessionCookie = $githubResponse['cookies'][$sessionCookieName] ?? null; + + if ($sessionCookie === null) { + $accountUnauth = $this->client->call(Client::METHOD_GET, '/account', $clientHeaders); + $this->assertSame(401, $accountUnauth['headers']['status-code']); + return; + } + + $accountResponse = $this->client->call(Client::METHOD_GET, '/account', \array_merge($clientHeaders, [ + 'cookie' => $sessionCookieName . '=' . $sessionCookie, + ])); + $this->assertSame(200, $accountResponse['headers']['status-code']); + $this->assertNotEmpty($accountResponse['body']['$id']); + } +} From d0f6daa67a38485e2ca742cb68d76f235541e39e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sat, 25 Apr 2026 12:05:35 +0200 Subject: [PATCH 20/51] Fix integration test --- docker-compose.yml | 2 ++ .../Project/OAuthGitHubIntegrationTest.php | 27 ++++++------------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7d53d2965d..da5efac438 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -247,6 +247,8 @@ services: - _APP_CUSTOM_DOMAIN_DENY_LIST - _APP_TRUSTED_HEADERS - _APP_MIGRATION_HOST + - _TESTS_OAUTH2_GITHUB_CLIENT_ID + - _TESTS_OAUTH2_GITHUB_CLIENT_SECRET extra_hosts: - "host.docker.internal:host-gateway" diff --git a/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php b/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php index 1a6f05ec6f..58123aeff3 100644 --- a/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php +++ b/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php @@ -94,8 +94,8 @@ class OAuthGitHubIntegrationTest extends Scope '/account/sessions/oauth2/github', $clientHeaders, [ - 'success' => 'http://localhost/v1/mock/tests/general/oauth2/success', - 'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure', + 'success' => 'http://localhost:4000/success', + 'failure' => 'http://localhost:4000/failure', ], followRedirects: false ); @@ -126,23 +126,12 @@ class OAuthGitHubIntegrationTest extends Scope // reached GitHub. Anything else means our redirect is malformed. $this->assertContains($githubResponse['headers']['status-code'], [200, 302]); - // Final step: GET /v1/account with the session cookie set by the OAuth callback. In an - // automated environment that completes the GitHub authorization step, the call below - // returns 200 with the OAuth user. Without that step (no GitHub login/approval automated - // here), there is no session cookie, so the call returns 401. - $sessionCookieName = 'a_session_' . $newProjectId; - $sessionCookie = $githubResponse['cookies'][$sessionCookieName] ?? null; + // Cleanup: delete the project + $deleteProject = $this->client->call(Client::METHOD_DELETE, '/projects/' . $newProjectId, $consoleHeaders); + $this->assertSame(204, $deleteProject['headers']['status-code']); - if ($sessionCookie === null) { - $accountUnauth = $this->client->call(Client::METHOD_GET, '/account', $clientHeaders); - $this->assertSame(401, $accountUnauth['headers']['status-code']); - return; - } - - $accountResponse = $this->client->call(Client::METHOD_GET, '/account', \array_merge($clientHeaders, [ - 'cookie' => $sessionCookieName . '=' . $sessionCookie, - ])); - $this->assertSame(200, $accountResponse['headers']['status-code']); - $this->assertNotEmpty($accountResponse['body']['$id']); + // Cleanup: delete the organization (team) + $deleteTeam = $this->client->call(Client::METHOD_DELETE, '/teams/' . $teamId, $consoleHeaders); + $this->assertSame(204, $deleteTeam['headers']['status-code']); } } From d25dac7d60f3eb2999c7ee9a3b1c948bf0400e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sun, 26 Apr 2026 10:29:41 +0200 Subject: [PATCH 21/51] Manual quality improvmenets --- app/init/models.php | 4 -- .../Http/Project/OAuth2/Amazon/Update.php | 4 +- .../Http/Project/OAuth2/Apple/Update.php | 6 +- .../Http/Project/OAuth2/Auth0/Update.php | 4 +- .../Http/Project/OAuth2/Authentik/Update.php | 4 +- .../Http/Project/OAuth2/Autodesk/Update.php | 4 +- .../Http/Project/OAuth2/Bitbucket/Update.php | 4 +- .../Http/Project/OAuth2/Bitly/Update.php | 4 +- .../Http/Project/OAuth2/Box/Update.php | 4 +- .../Project/OAuth2/Dailymotion/Update.php | 4 +- .../Http/Project/OAuth2/Discord/Update.php | 4 +- .../Http/Project/OAuth2/Disqus/Update.php | 4 +- .../Http/Project/OAuth2/Dropbox/Update.php | 4 +- .../Http/Project/OAuth2/Etsy/Update.php | 4 +- .../Http/Project/OAuth2/Facebook/Update.php | 4 +- .../Http/Project/OAuth2/Figma/Update.php | 4 +- .../Http/Project/OAuth2/GitHub/Update.php | 4 +- .../Http/Project/OAuth2/Gitlab/Update.php | 4 +- .../Http/Project/OAuth2/Google/Update.php | 4 +- .../Http/Project/OAuth2/Linkedin/Update.php | 4 +- .../Http/Project/OAuth2/Notion/Update.php | 4 +- .../Http/Project/OAuth2/Oidc/Update.php | 4 +- .../Http/Project/OAuth2/Paypal/Update.php | 4 +- .../Project/OAuth2/PaypalSandbox/Update.php | 25 +-------- .../Http/Project/OAuth2/Podio/Update.php | 4 +- .../Http/Project/OAuth2/Salesforce/Update.php | 4 +- .../Http/Project/OAuth2/Slack/Update.php | 4 +- .../Http/Project/OAuth2/Spotify/Update.php | 4 +- .../Http/Project/OAuth2/Stripe/Update.php | 4 +- .../Http/Project/OAuth2/Tradeshift/Update.php | 4 +- .../Project/OAuth2/TradeshiftBox/Update.php | 55 ------------------- .../OAuth2/TradeshiftSandbox/Update.php | 29 ++++++++++ .../Http/Project/OAuth2/Twitch/Update.php | 4 +- .../Http/Project/OAuth2/WordPress/Update.php | 4 +- .../Project/Http/Project/OAuth2/X/Update.php | 4 +- .../Http/Project/OAuth2/Yahoo/Update.php | 4 +- .../Http/Project/OAuth2/Yandex/Update.php | 4 +- .../Http/Project/OAuth2/Zoho/Update.php | 4 +- .../Http/Project/OAuth2/Zoom/Update.php | 4 +- src/Appwrite/Utopia/Response.php | 2 - .../Response/Model/OAuth2PaypalSandbox.php | 53 ------------------ .../Response/Model/OAuth2TradeshiftBox.php | 53 ------------------ 42 files changed, 102 insertions(+), 261 deletions(-) delete mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/TradeshiftBox/Update.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/TradeshiftSandbox/Update.php delete mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2PaypalSandbox.php delete mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2TradeshiftBox.php diff --git a/app/init/models.php b/app/init/models.php index 8d95e50d02..f24e2045df 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -127,14 +127,12 @@ use Appwrite\Utopia\Response\Model\OAuth2Linkedin; use Appwrite\Utopia\Response\Model\OAuth2Notion; use Appwrite\Utopia\Response\Model\OAuth2Oidc; use Appwrite\Utopia\Response\Model\OAuth2Paypal; -use Appwrite\Utopia\Response\Model\OAuth2PaypalSandbox; use Appwrite\Utopia\Response\Model\OAuth2Podio; use Appwrite\Utopia\Response\Model\OAuth2Salesforce; use Appwrite\Utopia\Response\Model\OAuth2Slack; use Appwrite\Utopia\Response\Model\OAuth2Spotify; use Appwrite\Utopia\Response\Model\OAuth2Stripe; use Appwrite\Utopia\Response\Model\OAuth2Tradeshift; -use Appwrite\Utopia\Response\Model\OAuth2TradeshiftBox; use Appwrite\Utopia\Response\Model\OAuth2Twitch; use Appwrite\Utopia\Response\Model\OAuth2WordPress; use Appwrite\Utopia\Response\Model\OAuth2X; @@ -416,9 +414,7 @@ Response::setModel(new OAuth2Amazon()); Response::setModel(new OAuth2Etsy()); Response::setModel(new OAuth2Facebook()); Response::setModel(new OAuth2Tradeshift()); -Response::setModel(new OAuth2TradeshiftBox()); Response::setModel(new OAuth2Paypal()); -Response::setModel(new OAuth2PaypalSandbox()); Response::setModel(new OAuth2Gitlab()); Response::setModel(new OAuth2Authentik()); Response::setModel(new OAuth2Auth0()); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php index 0129daf7f4..1542f3b3bc 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php @@ -35,11 +35,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of Amazon OAuth2 app. For example: amzn1.application-oa2-client.87400c00000000000000000000063d5b2'; + return '\'Client ID\' of Amazon OAuth2 app. For example: amzn1.application-oa2-client.87400c00000000000000000000063d5b2'; } public static function getClientSecretDescription(): string { - return 'Client Secret of Amazon OAuth2 app. For example: 79ffe4000000000000000000000000000000000000000000000000000002de55'; + return '\'Client Secret\' of Amazon OAuth2 app. For example: 79ffe4000000000000000000000000000000000000000000000000000002de55'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php index edbfdb8b9e..7a0cf59661 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php @@ -50,7 +50,7 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Service ID of Apple OAuth2 app. For example: ip.appwrite.app.web'; + return '\'Service ID\' of Apple OAuth2 app. For example: ip.appwrite.app.web'; } public static function getClientSecretDescription(): string @@ -88,8 +88,8 @@ class Update extends Base ], )) ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) - ->param('keyId', null, new Nullable(new Text(256, 0)), 'Key ID of Apple OAuth2 app. For example: P4000000N8', optional: true) - ->param('teamId', null, new Nullable(new Text(256, 0)), 'Team ID of Apple OAuth2 app. For example: D4000000R6', optional: true) + ->param('keyId', null, new Nullable(new Text(256, 0)), '\'Key ID\' of Apple OAuth2 app. For example: P4000000N8', optional: true) + ->param('teamId', null, new Nullable(new Text(256, 0)), '\'Team ID\' of Apple OAuth2 app. For example: D4000000R6', optional: true) ->param('p8File', null, new Nullable(new Text(4096, 0)), 'Contents of the Apple OAuth2 app .p8 private key file. The secret key wrapped by the PEM markers is 200 characters long. For example: -----BEGIN PRIVATE KEY-----MIGTAg...jy2Xbna-----END PRIVATE KEY-----', 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') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php index d551689d82..9fe0b1384d 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php @@ -45,12 +45,12 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of Auth0 OAuth2 app. For example: OaOkIA000000000000000000005KLSYq'; + return '\'Client ID\' of Auth0 OAuth2 app. For example: OaOkIA000000000000000000005KLSYq'; } public static function getClientSecretDescription(): string { - return 'Client Secret of Auth0 OAuth2 app. For example: zXz0000-00000000000000000000000000000-00000000000000000000PJafnF'; + return '\'Client Secret\' of Auth0 OAuth2 app. For example: zXz0000-00000000000000000000000000000-00000000000000000000PJafnF'; } public function __construct() diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php index 2b69319a71..48a7f1a22b 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php @@ -45,12 +45,12 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of Authentik OAuth2 app. For example: dTKOPa0000000000000000000000000000e7G8hv'; + return '\'Client ID\' of Authentik OAuth2 app. For example: dTKOPa0000000000000000000000000000e7G8hv'; } public static function getClientSecretDescription(): string { - return 'Client Secret of Authentik OAuth2 app. For example: ntQadq000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000Hp5WK'; + return '\'Client Secret\' of Authentik OAuth2 app. For example: ntQadq000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000Hp5WK'; } public function __construct() diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php index 6d959479f6..6331f23080 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php @@ -35,11 +35,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'client ID of Autodesk OAuth2 app. For example: 5zw90v00000000000000000000kVYXN7'; + return '\'client ID\' of Autodesk OAuth2 app. For example: 5zw90v00000000000000000000kVYXN7'; } public static function getClientSecretDescription(): string { - return 'client secret of Autodesk OAuth2 app. For example: 7I000000000000MW'; + return '\'client secret\' of Autodesk OAuth2 app. For example: 7I000000000000MW'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php index bc430101e5..cbb48445b5 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php @@ -45,11 +45,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Key of Bitbucket OAuth2 app. For example: Knt70000000000ByRc'; + return '\'Key\' of Bitbucket OAuth2 app. For example: Knt70000000000ByRc'; } public static function getClientSecretDescription(): string { - return 'Secret of Bitbucket OAuth2 app. For example: NMfLZJ00000000000000000000TLQdDx'; + return '\'Secret\' of Bitbucket OAuth2 app. For example: NMfLZJ00000000000000000000TLQdDx'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php index 9bb56ce221..d8964610e6 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php @@ -35,11 +35,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of Bitly OAuth2 app. For example: d95151000000000000000000000000000067af9b'; + return '\'Client ID\' of Bitly OAuth2 app. For example: d95151000000000000000000000000000067af9b'; } public static function getClientSecretDescription(): string { - return 'Client secret of Bitly OAuth2 app. For example: a13e250000000000000000000000000000d73095'; + return '\'Client Secret\' of Bitly OAuth2 app. For example: a13e250000000000000000000000000000d73095'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php index 306a7c8529..8cb9df835a 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php @@ -35,11 +35,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of Box OAuth2 app. For example: deglcs00000000000000000000x2og6y'; + return '\'Client ID\' of Box OAuth2 app. For example: deglcs00000000000000000000x2og6y'; } public static function getClientSecretDescription(): string { - return 'Client Secret of Box OAuth2 app. For example: OKM1f100000000000000000000eshEif'; + return '\'Client Secret\' of Box OAuth2 app. For example: OKM1f100000000000000000000eshEif'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php index 2d4cb3307a..d2f38309b4 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php @@ -45,11 +45,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'API key of Dailymotion OAuth2 app. For example: 07a9000000000000067f'; + return '\'API key\' of Dailymotion OAuth2 app. For example: 07a9000000000000067f'; } public static function getClientSecretDescription(): string { - return 'API secret of Dailymotion OAuth2 app. For example: a399a90000000000000000000000000000d90639'; + return '\'API secret\' of Dailymotion OAuth2 app. For example: a399a90000000000000000000000000000d90639'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php index 449ed1067f..5efc193019 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php @@ -35,11 +35,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of Discord OAuth2 app. For example: 950722000000343754'; + return '\'Client ID\' of Discord OAuth2 app. For example: 950722000000343754'; } public static function getClientSecretDescription(): string { - return 'Client Secret of Discord OAuth2 app. For example: YmPXnM000000000000000000002zFg5D'; + return '\'Client Secret\' of Discord OAuth2 app. For example: YmPXnM000000000000000000002zFg5D'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php index 50902c0263..e77cd9b152 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php @@ -45,11 +45,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Public key, also known as API Key, of Disqus OAuth2 app. For example: cgegH70000000000000000000000000000000000000000000000000000Hr1nYX'; + return '\'Public key\', also known as \'API Key\', of Disqus OAuth2 app. For example: cgegH70000000000000000000000000000000000000000000000000000Hr1nYX'; } public static function getClientSecretDescription(): string { - return 'Secret Key, also known as API Secret, of Disqus OAuth2 app. For example: W7Bykj00000000000000000000000000000000000000000000000000003o43w9'; + return '\'Secret Key\', also known as \'API Secret\', of Disqus OAuth2 app. For example: W7Bykj00000000000000000000000000000000000000000000000000003o43w9'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php index 27d2444955..385b7719df 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php @@ -45,11 +45,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'App key of Dropbox OAuth2 app. For example: jl000000000009t'; + return '\'App key\' of Dropbox OAuth2 app. For example: jl000000000009t'; } public static function getClientSecretDescription(): string { - return 'App secret of Dropbox OAuth2 app. For example: g200000000000vw'; + return '\'App secret\' of Dropbox OAuth2 app. For example: g200000000000vw'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php index 36d79d2c99..291daec414 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php @@ -45,11 +45,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Keystring of Etsy OAuth2 app. For example: nsgzxh0000000000008j85a2'; + return '\'Keystring\' of Etsy OAuth2 app. For example: nsgzxh0000000000008j85a2'; } public static function getClientSecretDescription(): string { - return 'Shared Secret of Etsy OAuth2 app. For example: tp000000ru'; + return '\'Shared Secret\' of Etsy OAuth2 app. For example: tp000000ru'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php index 9a435b6123..a3f97334a3 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php @@ -45,11 +45,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'App ID of Facebook OAuth2 app. For example: 260600000007694'; + return '\'App ID\' of Facebook OAuth2 app. For example: 260600000007694'; } public static function getClientSecretDescription(): string { - return 'App secret of Facebook OAuth2 app. For example: 2d0b2800000000000000000000d38af4'; + return '\'App secret\' of Facebook OAuth2 app. For example: 2d0b2800000000000000000000d38af4'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php index 2fa62a8428..b005bf17c9 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php @@ -35,11 +35,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of Figma OAuth2 app. For example: byay5H0000000000VtiI40'; + return '\'Client ID\' of Figma OAuth2 app. For example: byay5H0000000000VtiI40'; } public static function getClientSecretDescription(): string { - return 'Client Secret of Figma OAuth2 app. For example: yEpOYn0000000000000000004iIsU5'; + return '\'Client Secret\' of Figma OAuth2 app. For example: yEpOYn0000000000000000004iIsU5'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php index 04c6af54ee..3d4f77f117 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php @@ -35,11 +35,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of GitHub OAuth2 app, or App ID of GitHub generic app. For example: e4d87900000000540733'; + return '\'Client ID\' of GitHub OAuth2 app, or \'App ID\' of GitHub generic app. For example: e4d87900000000540733. Example of wrong value: 370006'; } public static function getClientSecretDescription(): string { - return 'Client secret of GitHub OAuth2 app, or GitHub generic app. For example: 5e07c00000000000000000000000000000198bcc'; + return '\'Client secret\' of GitHub OAuth2 app, or GitHub generic app. For example: 5e07c00000000000000000000000000000198bcc'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php index fafc97c836..ce7fa21ee1 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php @@ -56,12 +56,12 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Application ID of GitLab OAuth2 app. For example: d41ffe0000000000000000000000000000000000000000000000000000d5e252'; + return '\'Application ID\' of GitLab OAuth2 app. For example: d41ffe0000000000000000000000000000000000000000000000000000d5e252'; } public static function getClientSecretDescription(): string { - return 'Secret of GitLab OAuth2 app. For example: gloas-838cfa0000000000000000000000000000000000000000000000000000ecbb38'; + return '\'Secret\' of GitLab OAuth2 app. For example: gloas-838cfa0000000000000000000000000000000000000000000000000000ecbb38'; } public function __construct() diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php index f8d2cc21a2..796b6dae20 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php @@ -35,11 +35,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of Google OAuth2 app. For example: 120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com'; + return '\'Client ID\' of Google OAuth2 app. For example: 120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com'; } public static function getClientSecretDescription(): string { - return 'Client secret of Google OAuth2 app. For example: GOCSPX-2k8gsR0000000000000000VNahJj'; + return '\'Client secret\' of Google OAuth2 app. For example: GOCSPX-2k8gsR0000000000000000VNahJj'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php index 39ae950e03..f23908279e 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php @@ -40,11 +40,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of LinkedIn OAuth2 app. For example: 770000000000dv'; + return '\'Client ID\' of LinkedIn OAuth2 app. For example: 770000000000dv'; } public static function getClientSecretDescription(): string { - return 'Primary Client Secret, also known as Secondary Client Secret, of LinkedIn OAuth2 app. For example: WPL_AP1.2Bf0000000000000'; + return '\'Primary Client Secret\' or \'Secondary Client Secret\', of LinkedIn OAuth2 app. For example: WPL_AP1.2Bf0000000000000./HtlYw=='; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php index 5c8473d75d..56451166a4 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php @@ -45,11 +45,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'OAuth Client ID of Notion OAuth2 app. For example: 341d8700-0000-0000-0000-000000446ee3'; + return '\'OAuth Client ID\' of Notion OAuth2 app. For example: 341d8700-0000-0000-0000-000000446ee3'; } public static function getClientSecretDescription(): string { - return 'OAuth Client Secret of Notion OAuth2 app. For example: secret_dLUr4b000000000000000000000000000000lFHAa9'; + return '\'OAuth Client Secret\' of Notion OAuth2 app. For example: secret_dLUr4b000000000000000000000000000000lFHAa9'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php index d8f85bd6b6..39cf5b2f96 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php @@ -47,12 +47,12 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of OpenID Connect OAuth2 app. For example: qibI2x0000000000000000000000000006L2YFoG'; + return '\'Client ID\' of OpenID Connect OAuth2 app. For example: qibI2x0000000000000000000000000006L2YFoG'; } public static function getClientSecretDescription(): string { - return 'Client Secret of OpenID Connect OAuth2 app. For example: Ah68ed000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003qpcHV'; + return '\'Client Secret\' of OpenID Connect OAuth2 app. For example: Ah68ed000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003qpcHV'; } public function __construct() diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php index a223de70f5..36b50475da 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php @@ -40,11 +40,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of PayPal OAuth2 app. For example: AdhIEG7-000000000000-0000000000000000000000000000000-0000000000000000000000-2pyB'; + return '\'Client ID\' of ' . static::getProviderLabel() . ' OAuth2 app. For example: AdhIEG7-000000000000-0000000000000000000000000000000-0000000000000000000000-2pyB'; } public static function getClientSecretDescription(): string { - return 'Secret key 1, also known as Secret key 2, of PayPal OAuth2 app. For example: EH8KCXtew--000000000000000000000000000000000000000_C-1_5UP_000000000000000CB7KDp'; + return '\'Secret key 1\', or \'Secret key 2\', of ' . static::getProviderLabel() . ' OAuth2 app. For example: EH8KCXtew--000000000000000000000000000000000000000_C-1_5UP_000000000000000CB7KDp'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/PaypalSandbox/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/PaypalSandbox/Update.php index 0436074d6c..c9f40094d5 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/PaypalSandbox/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/PaypalSandbox/Update.php @@ -3,10 +3,9 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\PaypalSandbox; use Appwrite\Auth\OAuth2\PaypalSandbox; -use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base; -use Appwrite\Utopia\Response; +use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Paypal\Update as PaypalUpdate; -class Update extends Base +class Update extends PaypalUpdate { public static function getProviderId(): string { @@ -27,24 +26,4 @@ class Update extends Base { return 'updateOAuth2PaypalSandbox'; } - - public static function getResponseModel(): string - { - return Response::MODEL_OAUTH2_PAYPAL_SANDBOX; - } - - public static function getClientSecretParamName(): string - { - return 'secretKey'; - } - - public static function getClientIdDescription(): string - { - return 'Client ID of PayPal Sandbox OAuth2 app. For example: AdhIEG7-000000000000-0000000000000000000000000000000-0000000000000000000000-2pyB'; - } - - public static function getClientSecretDescription(): string - { - return 'Secret key 1, also known as Secret key 2, of PayPal Sandbox OAuth2 app. For example: EH8KCXtew--000000000000000000000000000000000000000_C-1_5UP_000000000000000CB7KDp'; - } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php index 9ad95ecef2..47efa8b32b 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php @@ -35,11 +35,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of Podio OAuth2 app. For example: appwrite-oauth-test-app'; + return '\'Client ID\' of Podio OAuth2 app. For example: appwrite-o0000000st-app'; } public static function getClientSecretDescription(): string { - return 'Client Secret of Podio OAuth2 app. For example: Rn247T0000000000000000000000000000000000000000000000000000W2zWTN'; + return '\'Client Secret\' of Podio OAuth2 app. For example: Rn247T0000000000000000000000000000000000000000000000000000W2zWTN'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php index be75dfa9f5..8721114327 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php @@ -45,11 +45,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Consumer key of Salesforce OAuth2 app. For example: 3MVG9I0000000000000000000000000000000000000000000000000000000000000000000000000C5Aejq'; + return '\'Consumer key\' of Salesforce OAuth2 app. For example: 3MVG9I0000000000000000000000000000000000000000000000000000000000000000000000000C5Aejq'; } public static function getClientSecretDescription(): string { - return 'Consumer secret of Salesforce OAuth2 app. For example: 3w000000000000e2'; + return '\'Consumer secret\' of Salesforce OAuth2 app. For example: 3w000000000000e2'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php index 589ecd16b3..612bb26968 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php @@ -35,11 +35,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of Slack OAuth2 app. For example: 23000000089.15000000000023'; + return '\'Client ID\' of Slack OAuth2 app. For example: 23000000089.15000000000023'; } public static function getClientSecretDescription(): string { - return 'Client Secret of Slack OAuth2 app. For example: 81656000000000000000000000f3d2fd'; + return '\'Client Secret\' of Slack OAuth2 app. For example: 81656000000000000000000000f3d2fd'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php index 58e54891e8..d28bfac8a2 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php @@ -35,11 +35,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Client ID of Spotify OAuth2 app. For example: 6ec271000000000000000000009beace'; + return '\'Client ID\' of Spotify OAuth2 app. For example: 6ec271000000000000000000009beace'; } public static function getClientSecretDescription(): string { - return 'Client secret of Spotify OAuth2 app. For example: db068a000000000000000000008b5b9f'; + return '\'Client secret\' of Spotify OAuth2 app. For example: db068a000000000000000000008b5b9f'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php index beed3737be..605804fa96 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php @@ -40,11 +40,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'client ID of Stripe OAuth2 app. For example: ca_UKibXX0000000000000000000006byvR'; + return '\'client ID\' of Stripe OAuth2 app. For example: ca_UKibXX0000000000000000000006byvR'; } public static function getClientSecretDescription(): string { - return 'API Secret key of Stripe OAuth2 app. For example: sk_51SfOd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000QGWYfp'; + return '\'API Secret key\' of Stripe OAuth2 app. For example: sk_51SfOd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000QGWYfp'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Tradeshift/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Tradeshift/Update.php index 7bb2b078e8..bff866cde6 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Tradeshift/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Tradeshift/Update.php @@ -45,11 +45,11 @@ class Update extends Base public static function getClientIdDescription(): string { - return 'Oauth2 Client ID of Tradeshift OAuth2 app. For example: appwrite-test-org.appwrite-test-app'; + return '\'Oauth2 Client ID\' of ' . static::getProviderLabel() . ' OAuth2 app. For example: appwrite-tes00000.0000000000est-app'; } public static function getClientSecretDescription(): string { - return 'Oauth2 Client secret of Tradeshift OAuth2 app. For example: 7cb52700-0000-0000-0000-000000ca5b83'; + return '\'Oauth2 Client secret\' of ' . static::getProviderLabel() . ' OAuth2 app. For example: 7cb52700-0000-0000-0000-000000ca5b83'; } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/TradeshiftBox/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/TradeshiftBox/Update.php deleted file mode 100644 index 3d153d408e..0000000000 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/TradeshiftBox/Update.php +++ /dev/null @@ -1,55 +0,0 @@ - Date: Sun, 26 Apr 2026 10:56:41 +0200 Subject: [PATCH 22/51] Make okta server ID optional --- src/Appwrite/Auth/OAuth2/Okta.php | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Auth/OAuth2/Okta.php b/src/Appwrite/Auth/OAuth2/Okta.php index 610d9847f2..13d420f6f2 100644 --- a/src/Appwrite/Auth/OAuth2/Okta.php +++ b/src/Appwrite/Auth/OAuth2/Okta.php @@ -42,7 +42,12 @@ class Okta extends OAuth2 */ public function getLoginURL(): string { - return 'https://' . $this->getOktaDomain() . '/oauth2/' . $this->getAuthorizationServerId() . '/v1/authorize?' . \http_build_query([ + $base = 'https://' . $this->getOktaDomain() . '/oauth2'; + if(!empty($this->getAuthorizationServerId())) { + $base .= '/' . $this->getAuthorizationServerId(); + } + + return $base . '/v1/authorize?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, 'state' => \json_encode($this->state), @@ -59,10 +64,15 @@ class Okta extends OAuth2 protected function getTokens(string $code): array { if (empty($this->tokens)) { + $base = 'https://' . $this->getOktaDomain() . '/oauth2'; + if(!empty($this->getAuthorizationServerId())) { + $base .= '/' . $this->getAuthorizationServerId(); + } + $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', - 'https://' . $this->getOktaDomain() . '/oauth2/' . $this->getAuthorizationServerId() . '/v1/token', + $base . '/v1/token', $headers, \http_build_query([ 'code' => $code, @@ -86,10 +96,15 @@ class Okta extends OAuth2 */ public function refreshTokens(string $refreshToken): array { + $base = 'https://' . $this->getOktaDomain() . '/oauth2'; + if(!empty($this->getAuthorizationServerId())) { + $base .= '/' . $this->getAuthorizationServerId(); + } + $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', - 'https://' . $this->getOktaDomain() . '/oauth2/' . $this->getAuthorizationServerId() . '/v1/token', + $base . '/v1/token', $headers, \http_build_query([ 'refresh_token' => $refreshToken, @@ -170,8 +185,13 @@ class Okta extends OAuth2 protected function getUser(string $accessToken): array { if (empty($this->user)) { + $base = 'https://' . $this->getOktaDomain() . '/oauth2'; + if(!empty($this->getAuthorizationServerId())) { + $base .= '/' . $this->getAuthorizationServerId(); + } + $headers = ['Authorization: Bearer ' . \urlencode($accessToken)]; - $user = $this->request('GET', 'https://' . $this->getOktaDomain() . '/oauth2/' . $this->getAuthorizationServerId() . '/v1/userinfo', $headers); + $user = $this->request('GET', $base . '/v1/userinfo', $headers); $this->user = \json_decode($user, true); } From 0a7b7de197fe31ddd39801c1afb002d78d67002e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sun, 26 Apr 2026 10:59:29 +0200 Subject: [PATCH 23/51] Revert changes - default works as fallback for optional serverID --- src/Appwrite/Auth/OAuth2/Okta.php | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/src/Appwrite/Auth/OAuth2/Okta.php b/src/Appwrite/Auth/OAuth2/Okta.php index 13d420f6f2..610d9847f2 100644 --- a/src/Appwrite/Auth/OAuth2/Okta.php +++ b/src/Appwrite/Auth/OAuth2/Okta.php @@ -42,12 +42,7 @@ class Okta extends OAuth2 */ public function getLoginURL(): string { - $base = 'https://' . $this->getOktaDomain() . '/oauth2'; - if(!empty($this->getAuthorizationServerId())) { - $base .= '/' . $this->getAuthorizationServerId(); - } - - return $base . '/v1/authorize?' . \http_build_query([ + return 'https://' . $this->getOktaDomain() . '/oauth2/' . $this->getAuthorizationServerId() . '/v1/authorize?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, 'state' => \json_encode($this->state), @@ -64,15 +59,10 @@ class Okta extends OAuth2 protected function getTokens(string $code): array { if (empty($this->tokens)) { - $base = 'https://' . $this->getOktaDomain() . '/oauth2'; - if(!empty($this->getAuthorizationServerId())) { - $base .= '/' . $this->getAuthorizationServerId(); - } - $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', - $base . '/v1/token', + 'https://' . $this->getOktaDomain() . '/oauth2/' . $this->getAuthorizationServerId() . '/v1/token', $headers, \http_build_query([ 'code' => $code, @@ -96,15 +86,10 @@ class Okta extends OAuth2 */ public function refreshTokens(string $refreshToken): array { - $base = 'https://' . $this->getOktaDomain() . '/oauth2'; - if(!empty($this->getAuthorizationServerId())) { - $base .= '/' . $this->getAuthorizationServerId(); - } - $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', - $base . '/v1/token', + 'https://' . $this->getOktaDomain() . '/oauth2/' . $this->getAuthorizationServerId() . '/v1/token', $headers, \http_build_query([ 'refresh_token' => $refreshToken, @@ -185,13 +170,8 @@ class Okta extends OAuth2 protected function getUser(string $accessToken): array { if (empty($this->user)) { - $base = 'https://' . $this->getOktaDomain() . '/oauth2'; - if(!empty($this->getAuthorizationServerId())) { - $base .= '/' . $this->getAuthorizationServerId(); - } - $headers = ['Authorization: Bearer ' . \urlencode($accessToken)]; - $user = $this->request('GET', $base . '/v1/userinfo', $headers); + $user = $this->request('GET', 'https://' . $this->getOktaDomain() . '/oauth2/' . $this->getAuthorizationServerId() . '/v1/userinfo', $headers); $this->user = \json_decode($user, true); } From e4bfb38a57bb12ceb34ad351db39167ab12c1fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Sun, 26 Apr 2026 11:14:50 +0200 Subject: [PATCH 24/51] add okta provider --- app/init/models.php | 2 + .../Http/Project/OAuth2/Okta/Update.php | 162 ++++++++++++++++++ .../Modules/Project/Services/Http.php | 2 + src/Appwrite/Utopia/Response.php | 1 + .../Utopia/Response/Model/OAuth2Okta.php | 62 +++++++ 5 files changed, 229 insertions(+) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Okta.php diff --git a/app/init/models.php b/app/init/models.php index f24e2045df..df2ebac150 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -126,6 +126,7 @@ use Appwrite\Utopia\Response\Model\OAuth2Google; use Appwrite\Utopia\Response\Model\OAuth2Linkedin; use Appwrite\Utopia\Response\Model\OAuth2Notion; use Appwrite\Utopia\Response\Model\OAuth2Oidc; +use Appwrite\Utopia\Response\Model\OAuth2Okta; use Appwrite\Utopia\Response\Model\OAuth2Paypal; use Appwrite\Utopia\Response\Model\OAuth2Podio; use Appwrite\Utopia\Response\Model\OAuth2Salesforce; @@ -419,6 +420,7 @@ Response::setModel(new OAuth2Gitlab()); Response::setModel(new OAuth2Authentik()); Response::setModel(new OAuth2Auth0()); Response::setModel(new OAuth2Oidc()); +Response::setModel(new OAuth2Okta()); Response::setModel(new OAuth2Apple()); Response::setModel(new PolicyPasswordDictionary()); Response::setModel(new PolicyPasswordHistory()); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php new file mode 100644 index 0000000000..dcbf1df343 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php @@ -0,0 +1,162 @@ +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: static::getProviderSDKMethod(), + description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: static::getResponseModel(), + ) + ], + )) + ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) + ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) + ->param('domain', null, new Nullable(new ValidatorDomain()), 'Okta company domain. Required when enabling the provider. For example: trial-6400025.okta.com. Example of wrong value: trial-6400025-admin.okta.com, or https://trial-6400025.okta.com/', optional: true) + ->param('authorizationServerId', null, new Nullable(new Text(256, 0)), 'Custom Authorization Servers. Optional, can be left empty or unconfigured. For example: aus000000000000000h7z', 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->handle(...)); + } + + /** + * Custom callback used instead of the parent's `action()` because Okta + * takes additional optional `domain` and `authorizationServerId` parameters. + * The method is named differently to avoid an LSP-incompatible override of + * Base::action(). + */ + public function handle( + ?string $clientId, + ?string $clientSecret, + ?string $domain, + ?string $authorizationServerId, + ?bool $enabled, + Response $response, + Database $dbForPlatform, + Document $project, + Authorization $authorization + ): void { + $providerId = static::getProviderId(); + + // The secret is stored as JSON `{"clientSecret": "...", "oktaDomain": "...", "authorizationServerId": "..."}` + // to match the shape Okta's OAuth2 adapter expects. + // Merge new values with existing storage so that submitting only some of + // the parameters leaves the others untouched. + $storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? ''; + $existing = []; + if (!empty($storedRaw)) { + $existing = \json_decode($storedRaw, true) ?: []; + } + + $encodedSecret = null; + if (!\is_null($clientSecret) || !\is_null($domain) || !\is_null($authorizationServerId)) { + $encodedSecret = \json_encode([ + 'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''), + 'oktaDomain' => $domain ?? ($existing['oktaDomain'] ?? ''), + 'authorizationServerId' => $authorizationServerId ?? ($existing['authorizationServerId'] ?? ''), + ]); + } + + // Domain is required when enabling the provider, since Okta builds its + // authorization, token and userinfo URLs from it. + if ($enabled === true) { + $effectiveDomain = $domain ?? ($existing['oktaDomain'] ?? ''); + if (empty($effectiveDomain)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain is required when enabling Okta OAuth2 provider.'); + } + } + + $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled); + + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; + $decoded = []; + if (!empty($storedRaw)) { + $decoded = \json_decode($storedRaw, true) ?: []; + } + + $response->dynamic(new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => $decoded['clientSecret'] ?? '', + 'domain' => $decoded['oktaDomain'] ?? '', + 'authorizationServerId' => $decoded['authorizationServerId'] ?? '', + ]), static::getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index c87e16107d..ec0ffe2997 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -36,6 +36,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Google\Update as Updat 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; +use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Okta\Update as UpdateOAuth2Okta; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Paypal\Update as UpdateOAuth2Paypal; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\PaypalSandbox\Update as UpdateOAuth2PaypalSandbox; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Podio\Update as UpdateOAuth2Podio; @@ -203,5 +204,6 @@ class Http extends Service $this->addAction(UpdateOAuth2Authentik::getName(), new UpdateOAuth2Authentik()); $this->addAction(UpdateOAuth2Auth0::getName(), new UpdateOAuth2Auth0()); $this->addAction(UpdateOAuth2Oidc::getName(), new UpdateOAuth2Oidc()); + $this->addAction(UpdateOAuth2Okta::getName(), new UpdateOAuth2Okta()); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 2046df3678..3d8902342f 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -313,6 +313,7 @@ class Response extends SwooleResponse public const MODEL_OAUTH2_AUTH0 = 'oAuth2Auth0'; public const MODEL_OAUTH2_OIDC = 'oAuth2Oidc'; public const MODEL_OAUTH2_APPLE = 'oAuth2Apple'; + public const MODEL_OAUTH2_OKTA = 'oAuth2Okta'; // Health public const MODEL_HEALTH_STATUS = 'healthStatus'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Okta.php b/src/Appwrite/Utopia/Response/Model/OAuth2Okta.php new file mode 100644 index 0000000000..a0f9a6a06b --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Okta.php @@ -0,0 +1,62 @@ +addRule('domain', [ + 'type' => self::TYPE_STRING, + 'description' => 'Okta OAuth 2 domain.', + 'default' => '', + 'example' => 'trial-6400025.okta.com', + ]); + + $this->addRule('authorizationServerId', [ + 'type' => self::TYPE_STRING, + 'description' => 'Okta OAuth 2 authorization server ID.', + 'default' => '', + 'example' => 'aus000000000000000h7z', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'OAuth2Okta'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_OAUTH2_OKTA; + } +} From 8ce7aa2abe8a0eb94ce7d0b83c65e66351dbe58a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 12:27:52 +0200 Subject: [PATCH 25/51] Fix crashing http --- src/Appwrite/Platform/Modules/Project/Services/Http.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index ec0ffe2997..83f85fd4ae 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -45,7 +45,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Slack\Update as Update use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Spotify\Update as UpdateOAuth2Spotify; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Stripe\Update as UpdateOAuth2Stripe; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Tradeshift\Update as UpdateOAuth2Tradeshift; -use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\TradeshiftBox\Update as UpdateOAuth2TradeshiftBox; +use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\TradeshiftSandbox\Update as UpdateOAuth2TradeshiftSandbox; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Twitch\Update as UpdateOAuth2Twitch; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\WordPress\Update as UpdateOAuth2WordPress; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\X\Update as UpdateOAuth2X; @@ -197,7 +197,7 @@ class Http extends Service $this->addAction(UpdateOAuth2Etsy::getName(), new UpdateOAuth2Etsy()); $this->addAction(UpdateOAuth2Facebook::getName(), new UpdateOAuth2Facebook()); $this->addAction(UpdateOAuth2Tradeshift::getName(), new UpdateOAuth2Tradeshift()); - $this->addAction(UpdateOAuth2TradeshiftBox::getName(), new UpdateOAuth2TradeshiftBox()); + $this->addAction(UpdateOAuth2TradeshiftSandbox::getName(), new UpdateOAuth2TradeshiftSandbox()); $this->addAction(UpdateOAuth2Paypal::getName(), new UpdateOAuth2Paypal()); $this->addAction(UpdateOAuth2PaypalSandbox::getName(), new UpdateOAuth2PaypalSandbox()); $this->addAction(UpdateOAuth2Gitlab::getName(), new UpdateOAuth2Gitlab()); From 2e960b90df1dc371e35942cec7e1573732ae957d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 13:38:26 +0200 Subject: [PATCH 26/51] Fix unused env variable --- app/controllers/general.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 2cec14cc1d..70bd323fb5 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -120,7 +120,7 @@ function router(Http $utopia, Database $dbForPlatform, callable $getProjectDB, S } } - if (!in_array($host, $platformHostnames)) { + if (!in_array($host, $platformHostnames) && System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'enabled') === 'enabled') { throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView); } From 15f94d99caecdf09da9e9071b4988cd91646e360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 14:02:30 +0200 Subject: [PATCH 27/51] Add Kick OAuth adapter --- app/config/oAuthProviders.php | 11 + app/init/models.php | 2 + src/Appwrite/Auth/OAuth2/Kick.php | 230 ++++++++++++++++++ .../Http/Project/OAuth2/Kick/Update.php | 45 ++++ .../Modules/Project/Services/Http.php | 2 + src/Appwrite/Utopia/Response.php | 1 + .../Utopia/Response/Model/OAuth2Kick.php | 43 ++++ 7 files changed, 334 insertions(+) create mode 100644 src/Appwrite/Auth/OAuth2/Kick.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Kick/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Kick.php diff --git a/app/config/oAuthProviders.php b/app/config/oAuthProviders.php index cda6459519..0dc2cb8b1e 100644 --- a/app/config/oAuthProviders.php +++ b/app/config/oAuthProviders.php @@ -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/', diff --git a/app/init/models.php b/app/init/models.php index df2ebac150..c439bdf28f 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -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()); diff --git a/src/Appwrite/Auth/OAuth2/Kick.php b/src/Appwrite/Auth/OAuth2/Kick.php new file mode 100644 index 0000000000..85b447fcd8 --- /dev/null +++ b/src/Appwrite/Auth/OAuth2/Kick.php @@ -0,0 +1,230 @@ +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|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)), '+/', '-_'), '='); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Kick/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Kick/Update.php new file mode 100644 index 0000000000..b5c126a08c --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Kick/Update.php @@ -0,0 +1,45 @@ +addAction(UpdateOAuth2Auth0::getName(), new UpdateOAuth2Auth0()); $this->addAction(UpdateOAuth2Oidc::getName(), new UpdateOAuth2Oidc()); $this->addAction(UpdateOAuth2Okta::getName(), new UpdateOAuth2Okta()); + $this->addAction(UpdateOAuth2Kick::getName(), new UpdateOAuth2Kick()); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 3d8902342f..1ac9054766 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -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'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Kick.php b/src/Appwrite/Utopia/Response/Model/OAuth2Kick.php new file mode 100644 index 0000000000..e4692ac6ea --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Kick.php @@ -0,0 +1,43 @@ + Date: Mon, 27 Apr 2026 14:09:24 +0200 Subject: [PATCH 28/51] Make oauth secret write only --- src/Appwrite/Utopia/Response/Model/AuthProvider.php | 4 ++-- src/Appwrite/Utopia/Response/Model/Project.php | 2 +- tests/e2e/Services/Projects/ProjectsConsoleClientTest.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/AuthProvider.php b/src/Appwrite/Utopia/Response/Model/AuthProvider.php index 2b8f962cd0..034be623e8 100644 --- a/src/Appwrite/Utopia/Response/Model/AuthProvider.php +++ b/src/Appwrite/Utopia/Response/Model/AuthProvider.php @@ -30,9 +30,9 @@ class AuthProvider extends Model ]) ->addRule('secret', [ 'type' => self::TYPE_STRING, - 'description' => 'OAuth 2.0 application secret. Might be JSON string if provider requires extra configuration.', + 'description' => 'OAuth 2.0 application secret. Might be JSON string if provider requires extra configuration. This property is write-only and always returned empty.', 'default' => '', - 'example' => 'Bpw_g9c2TGXxfgLshDbSaL8tsCcqgczQ', + 'example' => '', ]) ->addRule('enabled', [ 'type' => self::TYPE_BOOLEAN, diff --git a/src/Appwrite/Utopia/Response/Model/Project.php b/src/Appwrite/Utopia/Response/Model/Project.php index 97b58d8a51..36be3b751f 100644 --- a/src/Appwrite/Utopia/Response/Model/Project.php +++ b/src/Appwrite/Utopia/Response/Model/Project.php @@ -525,7 +525,7 @@ class Project extends Model 'key' => $key, 'name' => $provider['name'] ?? '', 'appId' => $providerValues[$key . 'Appid'] ?? '', - 'secret' => $providerValues[$key . 'Secret'] ?? '', + 'secret' => '', // Write-only: never expose the stored value 'enabled' => $providerValues[$key . 'Enabled'] ?? false, ]); } diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index f88db41e8c..8322e37de1 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -1634,7 +1634,7 @@ class ProjectsConsoleClientTest extends Scope foreach ($response['body']['oAuthProviders'] as $responseProvider) { if ($responseProvider['key'] === $key) { $this->assertEquals('AppId-' . ucfirst($key), $responseProvider['appId']); - $this->assertEquals('Secret-' . ucfirst($key), $responseProvider['secret']); + $this->assertEmpty($responseProvider['secret']); $this->assertFalse($responseProvider['enabled']); $asserted = true; break; From 2e57500d7e6063f180feb4516ca2bc84f17dabc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 14:16:43 +0200 Subject: [PATCH 29/51] WIP: Read endpoints for oauth --- .../Project/Http/Project/OAuth2/Get.php | 74 +++++++++++++++++ .../Project/Http/Project/OAuth2/XList.php | 79 +++++++++++++++++++ .../Modules/Project/Services/Http.php | 4 + 3 files changed, 157 insertions(+) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php new file mode 100644 index 0000000000..db7a19f51b --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php @@ -0,0 +1,74 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/project/oauth2/:provider') + ->desc('Get project OAuth2 provider') + ->groups(['api', 'project']) + ->label('scope', 'oauth2.read') + ->label('sdk', new Method( + namespace: 'project', + group: 'oauth2', + name: 'getOAuth2Provider', + description: <<param('provider', '', new Text(128), 'OAuth2 provider key. For example: github, google, apple.') + ->inject('response') + ->inject('project') + ->callback($this->action(...)); + } + + public function action( + string $provider, + Response $response, + Document $project, + ): void { + $providers = Config::getParam('oAuthProviders', []); + if (!\array_key_exists($provider, $providers) || !($providers[$provider]['enabled'] ?? false)) { + throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); + } + + $providerValues = $project->getAttribute('oAuthProviders', []); + + $response->dynamic(new Document([ + 'key' => $provider, + 'name' => $providers[$provider]['name'] ?? '', + 'appId' => $providerValues[$provider . 'Appid'] ?? '', + 'secret' => '', + 'enabled' => $providerValues[$provider . 'Enabled'] ?? false, + ]), Response::MODEL_AUTH_PROVIDER); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php new file mode 100644 index 0000000000..df0f436293 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php @@ -0,0 +1,79 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/project/oauth2') + ->desc('List project OAuth2 providers') + ->groups(['api', 'project']) + ->label('scope', 'oauth2.read') + ->label('sdk', new Method( + namespace: 'project', + group: 'oauth2', + name: 'listOAuth2Providers', + description: <<inject('response') + ->inject('project') + ->callback($this->action(...)); + } + + public function action( + Response $response, + Document $project, + ): void { + $providers = Config::getParam('oAuthProviders', []); + $providerValues = $project->getAttribute('oAuthProviders', []); + + $projectProviders = []; + foreach ($providers as $key => $provider) { + if (!($provider['enabled'] ?? false)) { + // Disabled by Appwrite configuration, exclude from response + continue; + } + + $projectProviders[] = new Document([ + 'key' => $key, + 'name' => $provider['name'] ?? '', + 'appId' => $providerValues[$key . 'Appid'] ?? '', + 'secret' => '', + 'enabled' => $providerValues[$key . 'Enabled'] ?? false, + ]); + } + + $response->dynamic(new Document([ + 'total' => \count($projectProviders), + 'platforms' => $projectProviders, + ]), Response::MODEL_AUTH_PROVIDER_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index 7c9424d34c..908e688367 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -30,6 +30,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Dropbox\Update as Upda use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Etsy\Update as UpdateOAuth2Etsy; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Facebook\Update as UpdateOAuth2Facebook; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Figma\Update as UpdateOAuth2Figma; +use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Get as GetOAuth2Provider; 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; @@ -50,6 +51,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\TradeshiftSandbox\Upda use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Twitch\Update as UpdateOAuth2Twitch; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\WordPress\Update as UpdateOAuth2WordPress; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\X\Update as UpdateOAuth2X; +use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\XList as ListOAuth2Providers; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Yahoo\Update as UpdateOAuth2Yahoo; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Yandex\Update as UpdateOAuth2Yandex; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Zoho\Update as UpdateOAuth2Zoho; @@ -169,6 +171,8 @@ class Http extends Service $this->addAction(UpdateAuthMethod::getName(), new UpdateAuthMethod()); // OAuth2 + $this->addAction(ListOAuth2Providers::getName(), new ListOAuth2Providers()); + $this->addAction(GetOAuth2Provider::getName(), new GetOAuth2Provider()); $this->addAction(UpdateOAuth2GitHub::getName(), new UpdateOAuth2GitHub()); $this->addAction(UpdateOAuth2Discord::getName(), new UpdateOAuth2Discord()); $this->addAction(UpdateOAuth2Figma::getName(), new UpdateOAuth2Figma()); From a781325679e2b0cb5591c5adb10ab7b4821c2a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 14:47:47 +0200 Subject: [PATCH 30/51] Add oauth read operations --- app/init/models.php | 2 + .../Http/Project/OAuth2/Apple/Update.php | 15 ++++ .../Http/Project/OAuth2/Auth0/Update.php | 15 ++++ .../Http/Project/OAuth2/Authentik/Update.php | 15 ++++ .../Project/Http/Project/OAuth2/Base.php | 90 +++++++++++++++++++ .../Project/Http/Project/OAuth2/Get.php | 58 +++++++++--- .../Http/Project/OAuth2/Gitlab/Update.php | 15 ++++ .../Http/Project/OAuth2/Oidc/Update.php | 18 ++++ .../Http/Project/OAuth2/Okta/Update.php | 16 ++++ .../Project/Http/Project/OAuth2/XList.php | 27 +++--- src/Appwrite/Utopia/Response.php | 1 + .../Utopia/Response/Model/OAuth2Apple.php | 10 +-- .../Utopia/Response/Model/OAuth2Auth0.php | 2 +- .../Utopia/Response/Model/OAuth2Authentik.php | 2 +- .../Utopia/Response/Model/OAuth2Base.php | 6 +- .../Utopia/Response/Model/OAuth2Gitlab.php | 2 +- .../Utopia/Response/Model/OAuth2Okta.php | 4 +- .../Response/Model/OAuth2ProviderList.php | 75 ++++++++++++++++ 18 files changed, 334 insertions(+), 39 deletions(-) create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php diff --git a/app/init/models.php b/app/init/models.php index c439bdf28f..20272db413 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -130,6 +130,7 @@ use Appwrite\Utopia\Response\Model\OAuth2Oidc; use Appwrite\Utopia\Response\Model\OAuth2Okta; use Appwrite\Utopia\Response\Model\OAuth2Paypal; use Appwrite\Utopia\Response\Model\OAuth2Podio; +use Appwrite\Utopia\Response\Model\OAuth2ProviderList; use Appwrite\Utopia\Response\Model\OAuth2Salesforce; use Appwrite\Utopia\Response\Model\OAuth2Slack; use Appwrite\Utopia\Response\Model\OAuth2Spotify; @@ -424,6 +425,7 @@ Response::setModel(new OAuth2Oidc()); Response::setModel(new OAuth2Okta()); Response::setModel(new OAuth2Kick()); Response::setModel(new OAuth2Apple()); +Response::setModel(new OAuth2ProviderList()); Response::setModel(new PolicyPasswordDictionary()); Response::setModel(new PolicyPasswordHistory()); Response::setModel(new PolicyPasswordPersonalData()); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php index 7a0cf59661..4f8437ce8d 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php @@ -99,6 +99,21 @@ class Update extends Base ->callback($this->handle(...)); } + public function buildReadResponse(Document $project): Document + { + $providerId = static::getProviderId(); + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + + return new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + 'keyId' => '', + 'teamId' => '', + 'p8File' => '', + ]); + } + /** * Custom callback used instead of the parent's `action()` because Apple's * client secret is composed of three fields (.p8 file contents, Key ID and diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php index 9fe0b1384d..1bbdd02a0d 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php @@ -91,6 +91,21 @@ class Update extends Base ->callback($this->handle(...)); } + public function buildReadResponse(Document $project): Document + { + $providerId = static::getProviderId(); + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $decoded = $this->decodeStoredSecret($project); + + return new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => '', + 'endpoint' => $decoded['auth0Domain'] ?? '', + ]); + } + /** * Custom callback used instead of the parent's `action()` because Auth0 * takes an additional optional `endpoint` parameter. The method is named diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php index 48a7f1a22b..62e314053a 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php @@ -91,6 +91,21 @@ class Update extends Base ->callback($this->handle(...)); } + public function buildReadResponse(Document $project): Document + { + $providerId = static::getProviderId(); + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $decoded = $this->decodeStoredSecret($project); + + return new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => '', + 'endpoint' => $decoded['authentikDomain'] ?? '', + ]); + } + /** * Custom callback used instead of the parent's `action()` because Authentik * takes an additional required `endpoint` parameter. The method is named diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php index 2d74c1b61d..f0aa50a695 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php @@ -137,6 +137,96 @@ abstract class Base extends Action ->callback($this->action(...)); } + /** + * Registry of provider ID -> Update action class. Mirrors the OAuth2 + * actions registered in Project\Services\Http. Used by the Get and XList + * read endpoints to dispatch per-provider response shaping. + * + * @return array> + */ + public static function getProviderActions(): array + { + return [ + 'github' => GitHub\Update::class, + 'discord' => Discord\Update::class, + 'figma' => Figma\Update::class, + 'dropbox' => Dropbox\Update::class, + 'dailymotion' => Dailymotion\Update::class, + 'bitbucket' => Bitbucket\Update::class, + 'bitly' => Bitly\Update::class, + 'box' => Box\Update::class, + 'autodesk' => Autodesk\Update::class, + 'google' => Google\Update::class, + 'zoom' => Zoom\Update::class, + 'zoho' => Zoho\Update::class, + 'yandex' => Yandex\Update::class, + 'x' => X\Update::class, + 'wordpress' => WordPress\Update::class, + 'twitch' => Twitch\Update::class, + 'stripe' => Stripe\Update::class, + 'spotify' => Spotify\Update::class, + 'slack' => Slack\Update::class, + 'podio' => Podio\Update::class, + 'notion' => Notion\Update::class, + 'salesforce' => Salesforce\Update::class, + 'yahoo' => Yahoo\Update::class, + 'linkedin' => Linkedin\Update::class, + 'disqus' => Disqus\Update::class, + 'amazon' => Amazon\Update::class, + 'etsy' => Etsy\Update::class, + 'facebook' => Facebook\Update::class, + 'tradeshift' => Tradeshift\Update::class, + 'tradeshiftSandbox' => TradeshiftSandbox\Update::class, + 'paypal' => Paypal\Update::class, + 'paypalSandbox' => PaypalSandbox\Update::class, + 'gitlab' => Gitlab\Update::class, + 'authentik' => Authentik\Update::class, + 'auth0' => Auth0\Update::class, + 'oidc' => Oidc\Update::class, + 'okta' => Okta\Update::class, + 'kick' => Kick\Update::class, + 'apple' => Apple\Update::class, + ]; + } + + /** + * Build the read-only response document for this provider, with credential + * fields zeroed out (write-only). Default implementation handles providers + * that store a plain client ID + client secret. Special providers (Apple, + * Gitlab, Auth0, Authentik, Oidc, Okta) override to expose their + * non-secret extras (endpoint, domain, discovery URLs, ...) decoded from + * the JSON-encoded secret blob. + */ + public function buildReadResponse(Document $project): Document + { + $providerId = static::getProviderId(); + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + + return new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => '', + ]); + } + + /** + * Decode the JSON-encoded secret blob stored under `{providerId}Secret`. + * Returns an empty array when the value is empty or not valid JSON. + */ + protected function decodeStoredSecret(Document $project): array + { + $providerId = static::getProviderId(); + $stored = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? ''; + + if (empty($stored)) { + return []; + } + + $decoded = \json_decode($stored, true); + return \is_array($decoded) ? $decoded : []; + } + /** * Apply the provided credential changes to the project's oAuthProviders map, * run the optional credential verification hook, persist the project, and diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php index db7a19f51b..29db552e46 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php @@ -35,13 +35,51 @@ class Get extends Action group: 'oauth2', name: 'getOAuth2Provider', description: <<getAttribute('oAuthProviders', []); + $actions = Base::getProviderActions(); + if (!isset($actions[$provider])) { + throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED); + } - $response->dynamic(new Document([ - 'key' => $provider, - 'name' => $providers[$provider]['name'] ?? '', - 'appId' => $providerValues[$provider . 'Appid'] ?? '', - 'secret' => '', - 'enabled' => $providerValues[$provider . 'Enabled'] ?? false, - ]), Response::MODEL_AUTH_PROVIDER); + $updateClass = $actions[$provider]; + $action = new $updateClass(); + + $response->dynamic($action->buildReadResponse($project), $updateClass::getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php index ce7fa21ee1..8d4f4e88da 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php @@ -102,6 +102,21 @@ class Update extends Base ->callback($this->handle(...)); } + public function buildReadResponse(Document $project): Document + { + $providerId = static::getProviderId(); + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $decoded = $this->decodeStoredSecret($project); + + return new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => '', + 'endpoint' => $decoded['endpoint'] ?? '', + ]); + } + /** * Custom callback used instead of the parent's `action()` because Gitlab * takes an additional `endpoint` parameter. The method is named diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php index 39cf5b2f96..d849e18efd 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php @@ -96,6 +96,24 @@ class Update extends Base ->callback($this->handle(...)); } + public function buildReadResponse(Document $project): Document + { + $providerId = static::getProviderId(); + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $decoded = $this->decodeStoredSecret($project); + + return new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => '', + 'wellKnownURL' => $decoded['wellKnownEndpoint'] ?? '', + 'authorizationURL' => $decoded['authorizationEndpoint'] ?? '', + 'tokenUrl' => $decoded['tokenEndpoint'] ?? '', + 'userInfoUrl' => $decoded['userInfoEndpoint'] ?? '', + ]); + } + /** * Custom callback used instead of the parent's `action()` because OIDC takes * a well-known URL plus three discovery URLs (authorization, token, user diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php index dcbf1df343..47d6cb2add 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php @@ -94,6 +94,22 @@ class Update extends Base ->callback($this->handle(...)); } + public function buildReadResponse(Document $project): Document + { + $providerId = static::getProviderId(); + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $decoded = $this->decodeStoredSecret($project); + + return new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => '', + 'domain' => $decoded['oktaDomain'] ?? '', + 'authorizationServerId' => $decoded['authorizationServerId'] ?? '', + ]); + } + /** * Custom callback used instead of the parent's `action()` because Okta * takes additional optional `domain` and `authorizationServerId` parameters. diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php index df0f436293..d0780e4bae 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/XList.php @@ -33,13 +33,13 @@ class XList extends Action group: 'oauth2', name: 'listOAuth2Providers', description: <<getAttribute('oAuthProviders', []); + $actions = Base::getProviderActions(); - $projectProviders = []; - foreach ($providers as $key => $provider) { - if (!($provider['enabled'] ?? false)) { + $documents = []; + foreach ($actions as $providerId => $updateClass) { + if (!($providers[$providerId]['enabled'] ?? false)) { // Disabled by Appwrite configuration, exclude from response continue; } - $projectProviders[] = new Document([ - 'key' => $key, - 'name' => $provider['name'] ?? '', - 'appId' => $providerValues[$key . 'Appid'] ?? '', - 'secret' => '', - 'enabled' => $providerValues[$key . 'Enabled'] ?? false, - ]); + $action = new $updateClass(); + $documents[] = $action->buildReadResponse($project); } $response->dynamic(new Document([ - 'total' => \count($projectProviders), - 'platforms' => $projectProviders, - ]), Response::MODEL_AUTH_PROVIDER_LIST); + 'total' => \count($documents), + 'providers' => $documents, + ]), Response::MODEL_OAUTH2_PROVIDER_LIST); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 1ac9054766..b8948a062e 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -315,6 +315,7 @@ class Response extends SwooleResponse public const MODEL_OAUTH2_APPLE = 'oAuth2Apple'; public const MODEL_OAUTH2_OKTA = 'oAuth2Okta'; public const MODEL_OAUTH2_KICK = 'oAuth2Kick'; + public const MODEL_OAUTH2_PROVIDER_LIST = 'oAuth2ProviderList'; // Health public const MODEL_HEALTH_STATUS = 'healthStatus'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Apple.php b/src/Appwrite/Utopia/Response/Model/OAuth2Apple.php index 8120090420..080925e6d8 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Apple.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Apple.php @@ -35,13 +35,13 @@ class OAuth2Apple extends OAuth2Base public function __construct() { - // Apple's OAuth 2 app credential is split into three fields (.p8 file + // Apple's OAuth2 app credential is split into three fields (.p8 file // contents, Key ID, Team ID) instead of a single clientSecret, so the // rules are defined manually rather than delegating to OAuth2Base. $this ->addRule('enabled', [ 'type' => self::TYPE_BOOLEAN, - 'description' => 'OAuth 2 provider is active and can be used to create sessions.', + 'description' => 'OAuth2 provider is active and can be used to create sessions.', 'default' => false, 'example' => false, ]) @@ -53,19 +53,19 @@ class OAuth2Apple extends OAuth2Base ]) ->addRule('keyId', [ 'type' => self::TYPE_STRING, - 'description' => 'Apple OAuth 2 key ID.', + 'description' => 'Apple OAuth2 key ID.', 'default' => '', 'example' => 'P4000000N8', ]) ->addRule('teamId', [ 'type' => self::TYPE_STRING, - 'description' => 'Apple OAuth 2 team ID.', + 'description' => 'Apple OAuth2 team ID.', 'default' => '', 'example' => 'D4000000R6', ]) ->addRule('p8File', [ 'type' => self::TYPE_STRING, - 'description' => 'Apple OAuth 2 .p8 private key file contents. The secret key wrapped by the PEM markers is 200 characters long.', + 'description' => 'Apple OAuth2 .p8 private key file contents. The secret key wrapped by the PEM markers is 200 characters long.', 'default' => '', 'example' => '-----BEGIN PRIVATE KEY-----MIGTAg...jy2Xbna-----END PRIVATE KEY-----', ]); diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Auth0.php b/src/Appwrite/Utopia/Response/Model/OAuth2Auth0.php index 89cf1c92d5..2f1893f4d5 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Auth0.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Auth0.php @@ -27,7 +27,7 @@ class OAuth2Auth0 extends OAuth2Base $this->addRule('endpoint', [ 'type' => self::TYPE_STRING, - 'description' => 'Auth0 OAuth 2 endpoint domain.', + 'description' => 'Auth0 OAuth2 endpoint domain.', 'default' => '', 'example' => 'example.us.auth0.com', ]); diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Authentik.php b/src/Appwrite/Utopia/Response/Model/OAuth2Authentik.php index ca6e828ed4..4e67e1f4fe 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Authentik.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Authentik.php @@ -27,7 +27,7 @@ class OAuth2Authentik extends OAuth2Base $this->addRule('endpoint', [ 'type' => self::TYPE_STRING, - 'description' => 'Authentik OAuth 2 endpoint domain.', + 'description' => 'Authentik OAuth2 endpoint domain.', 'default' => '', 'example' => 'example.authentik.com', ]); diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Base.php b/src/Appwrite/Utopia/Response/Model/OAuth2Base.php index b0bd642b34..8eb8d0f4cb 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Base.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Base.php @@ -79,7 +79,7 @@ abstract class OAuth2Base extends Model */ public function getClientIdDescription(): string { - return $this->getProviderLabel() . ' OAuth 2 ' . $this->getClientIdLabel() . '.'; + return $this->getProviderLabel() . ' OAuth2 ' . $this->getClientIdLabel() . '.'; } /** @@ -91,7 +91,7 @@ abstract class OAuth2Base extends Model */ public function getClientSecretDescription(): string { - return $this->getProviderLabel() . ' OAuth 2 ' . $this->getClientSecretLabel() . '.'; + return $this->getProviderLabel() . ' OAuth2 ' . $this->getClientSecretLabel() . '.'; } public function __construct() @@ -99,7 +99,7 @@ abstract class OAuth2Base extends Model $this ->addRule('enabled', [ 'type' => self::TYPE_BOOLEAN, - 'description' => 'OAuth 2 provider is active and can be used to create sessions.', + 'description' => 'OAuth2 provider is active and can be used to create sessions.', 'default' => false, 'example' => false, ]) diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Gitlab.php b/src/Appwrite/Utopia/Response/Model/OAuth2Gitlab.php index bae60c2f5d..41c91acfe8 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Gitlab.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Gitlab.php @@ -47,7 +47,7 @@ class OAuth2Gitlab extends OAuth2Base $this->addRule('endpoint', [ 'type' => self::TYPE_STRING, - 'description' => 'GitLab OAuth 2 endpoint URL. Defaults to https://gitlab.com for self-hosted instances.', + 'description' => 'GitLab OAuth2 endpoint URL. Defaults to https://gitlab.com for self-hosted instances.', 'default' => '', 'example' => 'https://gitlab.com', ]); diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Okta.php b/src/Appwrite/Utopia/Response/Model/OAuth2Okta.php index a0f9a6a06b..f0926193d8 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Okta.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Okta.php @@ -27,14 +27,14 @@ class OAuth2Okta extends OAuth2Base $this->addRule('domain', [ 'type' => self::TYPE_STRING, - 'description' => 'Okta OAuth 2 domain.', + 'description' => 'Okta OAuth2 domain.', 'default' => '', 'example' => 'trial-6400025.okta.com', ]); $this->addRule('authorizationServerId', [ 'type' => self::TYPE_STRING, - 'description' => 'Okta OAuth 2 authorization server ID.', + 'description' => 'Okta OAuth2 authorization server ID.', 'default' => '', 'example' => 'aus000000000000000h7z', ]); diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php b/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php new file mode 100644 index 0000000000..fd6ad1355b --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php @@ -0,0 +1,75 @@ +addRule('total', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total number of OAuth2 providers in the given project.', + 'default' => 0, + 'example' => 5, + ]) + ->addRule('providers', [ + 'type' => [ + Response::MODEL_OAUTH2_GITHUB, + Response::MODEL_OAUTH2_DISCORD, + Response::MODEL_OAUTH2_FIGMA, + Response::MODEL_OAUTH2_DROPBOX, + Response::MODEL_OAUTH2_DAILYMOTION, + Response::MODEL_OAUTH2_BITBUCKET, + Response::MODEL_OAUTH2_BITLY, + Response::MODEL_OAUTH2_BOX, + Response::MODEL_OAUTH2_AUTODESK, + Response::MODEL_OAUTH2_GOOGLE, + Response::MODEL_OAUTH2_ZOOM, + Response::MODEL_OAUTH2_ZOHO, + Response::MODEL_OAUTH2_YANDEX, + Response::MODEL_OAUTH2_X, + Response::MODEL_OAUTH2_WORDPRESS, + Response::MODEL_OAUTH2_TWITCH, + Response::MODEL_OAUTH2_STRIPE, + Response::MODEL_OAUTH2_SPOTIFY, + Response::MODEL_OAUTH2_SLACK, + Response::MODEL_OAUTH2_PODIO, + Response::MODEL_OAUTH2_NOTION, + Response::MODEL_OAUTH2_SALESFORCE, + Response::MODEL_OAUTH2_YAHOO, + Response::MODEL_OAUTH2_LINKEDIN, + Response::MODEL_OAUTH2_DISQUS, + Response::MODEL_OAUTH2_AMAZON, + Response::MODEL_OAUTH2_ETSY, + Response::MODEL_OAUTH2_FACEBOOK, + Response::MODEL_OAUTH2_TRADESHIFT, + Response::MODEL_OAUTH2_PAYPAL, + Response::MODEL_OAUTH2_GITLAB, + Response::MODEL_OAUTH2_AUTHENTIK, + Response::MODEL_OAUTH2_AUTH0, + Response::MODEL_OAUTH2_OIDC, + Response::MODEL_OAUTH2_APPLE, + Response::MODEL_OAUTH2_OKTA, + Response::MODEL_OAUTH2_KICK, + ], + 'description' => 'List of OAuth2 providers.', + 'default' => [], + 'array' => true, + ]) + ; + } + + public function getName(): string + { + return 'OAuth2 Providers List'; + } + + public function getType(): string + { + return Response::MODEL_OAUTH2_PROVIDER_LIST; + } +} From b28b851bb23a308c5244f615c83571d818e13579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 15:49:44 +0200 Subject: [PATCH 31/51] microsoft oauth endpoint --- app/init/models.php | 2 + .../Project/Http/Project/OAuth2/Base.php | 1 + .../Project/Http/Project/OAuth2/Get.php | 1 + .../Http/Project/OAuth2/Microsoft/Update.php | 167 ++++++++++++++++++ .../Modules/Project/Services/Http.php | 2 + src/Appwrite/Utopia/Response.php | 1 + .../Utopia/Response/Model/OAuth2Microsoft.php | 75 ++++++++ .../Response/Model/OAuth2ProviderList.php | 1 + 8 files changed, 250 insertions(+) create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Microsoft.php diff --git a/app/init/models.php b/app/init/models.php index 20272db413..1f92c77cec 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -125,6 +125,7 @@ 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\OAuth2Microsoft; use Appwrite\Utopia\Response\Model\OAuth2Notion; use Appwrite\Utopia\Response\Model\OAuth2Oidc; use Appwrite\Utopia\Response\Model\OAuth2Okta; @@ -425,6 +426,7 @@ Response::setModel(new OAuth2Oidc()); Response::setModel(new OAuth2Okta()); Response::setModel(new OAuth2Kick()); Response::setModel(new OAuth2Apple()); +Response::setModel(new OAuth2Microsoft()); Response::setModel(new OAuth2ProviderList()); Response::setModel(new PolicyPasswordDictionary()); Response::setModel(new PolicyPasswordHistory()); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php index f0aa50a695..ddaac7c602 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php @@ -186,6 +186,7 @@ abstract class Base extends Action 'okta' => Okta\Update::class, 'kick' => Kick\Update::class, 'apple' => Apple\Update::class, + 'microsoft' => Microsoft\Update::class, ]; } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php index 29db552e46..419d80f829 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php @@ -79,6 +79,7 @@ class Get extends Action Response::MODEL_OAUTH2_APPLE, Response::MODEL_OAUTH2_OKTA, Response::MODEL_OAUTH2_KICK, + Response::MODEL_OAUTH2_MICROSOFT, ], ) ] diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php new file mode 100644 index 0000000000..60479cf5f5 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php @@ -0,0 +1,167 @@ +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: static::getProviderSDKMethod(), + description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: static::getResponseModel(), + ) + ], + )) + ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) + ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) + ->param('tenant', '', new Text(256, 1), 'Microsoft Entra ID tenant identifier. Use \'common\', \'organizations\', \'consumers\' or a specific tenant ID. For example: common', optional: false) + ->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->handle(...)); + } + + public function buildReadResponse(Document $project): Document + { + $providerId = static::getProviderId(); + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $decoded = $this->decodeStoredSecret($project); + + return new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => '', + 'tenant' => $decoded['tenantID'] ?? '', + ]); + } + + /** + * Custom callback used instead of the parent's `action()` because Microsoft + * takes an additional required `tenant` parameter. The method is named + * differently to avoid an LSP-incompatible override of Base::action(). + */ + public function handle( + ?string $applicationId, + ?string $applicationSecret, + string $tenant, + ?bool $enabled, + Response $response, + Database $dbForPlatform, + Document $project, + Authorization $authorization + ): void { + $providerId = static::getProviderId(); + + // The secret is stored as JSON `{"clientSecret": "...", "tenantID": "..."}` + // to match the shape Microsoft's OAuth2 adapter expects (getTenantID()). + // The `tenant` param is required on every call, so it's always written. + // `applicationSecret` is optional; if omitted, the existing stored secret is preserved. + $storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? ''; + $existing = []; + if (!empty($storedRaw)) { + $existing = \json_decode($storedRaw, true) ?: []; + } + $encodedSecret = \json_encode([ + 'clientSecret' => $applicationSecret ?? ($existing['clientSecret'] ?? ''), + 'tenantID' => $tenant, + ]); + + $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $applicationId, $encodedSecret, $enabled); + + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; + $decoded = []; + if (!empty($storedRaw)) { + $decoded = \json_decode($storedRaw, true) ?: []; + } + + $response->dynamic(new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => $decoded['clientSecret'] ?? '', + 'tenant' => $decoded['tenantID'] ?? '', + ]), static::getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index 908e688367..8a330ca041 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -36,6 +36,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Gitlab\Update as Updat 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\Microsoft\Update as UpdateOAuth2Microsoft; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Notion\Update as UpdateOAuth2Notion; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Oidc\Update as UpdateOAuth2Oidc; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Okta\Update as UpdateOAuth2Okta; @@ -211,5 +212,6 @@ class Http extends Service $this->addAction(UpdateOAuth2Oidc::getName(), new UpdateOAuth2Oidc()); $this->addAction(UpdateOAuth2Okta::getName(), new UpdateOAuth2Okta()); $this->addAction(UpdateOAuth2Kick::getName(), new UpdateOAuth2Kick()); + $this->addAction(UpdateOAuth2Microsoft::getName(), new UpdateOAuth2Microsoft()); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index b8948a062e..4dbcf135af 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -315,6 +315,7 @@ class Response extends SwooleResponse public const MODEL_OAUTH2_APPLE = 'oAuth2Apple'; public const MODEL_OAUTH2_OKTA = 'oAuth2Okta'; public const MODEL_OAUTH2_KICK = 'oAuth2Kick'; + public const MODEL_OAUTH2_MICROSOFT = 'oAuth2Microsoft'; public const MODEL_OAUTH2_PROVIDER_LIST = 'oAuth2ProviderList'; // Health diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Microsoft.php b/src/Appwrite/Utopia/Response/Model/OAuth2Microsoft.php new file mode 100644 index 0000000000..30cd8da2f5 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Microsoft.php @@ -0,0 +1,75 @@ +addRule('tenant', [ + 'type' => self::TYPE_STRING, + 'description' => 'Microsoft Entra ID tenant identifier. Use \'common\', \'organizations\', \'consumers\' or a specific tenant ID.', + 'default' => '', + 'example' => 'common', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'OAuth2Microsoft'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_OAUTH2_MICROSOFT; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php b/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php index fd6ad1355b..5d1fb16a9a 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php @@ -55,6 +55,7 @@ class OAuth2ProviderList extends Model Response::MODEL_OAUTH2_APPLE, Response::MODEL_OAUTH2_OKTA, Response::MODEL_OAUTH2_KICK, + Response::MODEL_OAUTH2_MICROSOFT, ], 'description' => 'List of OAuth2 providers.', 'default' => [], From ee1eea5c0cb5fd8039b40aaaedbddb6166919dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 15:51:54 +0200 Subject: [PATCH 32/51] oauth tests setup --- tests/e2e/Services/Project/OAuth2Base.php | 8 ++++++++ .../Services/Project/OAuth2ConsoleClientTest.php | 14 ++++++++++++++ .../Services/Project/OAuth2CustomServerTest.php | 14 ++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 tests/e2e/Services/Project/OAuth2Base.php create mode 100644 tests/e2e/Services/Project/OAuth2ConsoleClientTest.php create mode 100644 tests/e2e/Services/Project/OAuth2CustomServerTest.php diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php new file mode 100644 index 0000000000..5c42ecc368 --- /dev/null +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -0,0 +1,8 @@ + Date: Mon, 27 Apr 2026 16:02:19 +0200 Subject: [PATCH 33/51] Add OAUth update tests --- tests/e2e/Services/Project/OAuth2Base.php | 1063 ++++++++++++++++++++- 1 file changed, 1062 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index 5c42ecc368..76f011e283 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -2,7 +2,1068 @@ namespace Tests\E2E\Services\Project; +use PHPUnit\Framework\Attributes\Before; +use Tests\E2E\Client; + trait OAuth2Base { - + /** + * Providers that follow the default `clientId` + `clientSecret` shape and + * have no extra required parameters. We use Amazon as the canonical sample + * for behavior tests because it has no `verifyCredentials()` hook, so we + * can freely enable/disable without making real network calls. + */ + protected static string $plainProvider = 'amazon'; + + /** + * Reset providers we mutate in tests back to a known empty/disabled state. + * The ProjectCustom trait reuses the same project across tests in a class, + * and the OAuth2 PATCH endpoint is additive (omitted fields are preserved), + * so without a reset state would leak between tests. + */ + #[Before(priority: -1)] + protected function resetProjectOAuth2(): void + { + $this->updateOAuth2($this->plainProvider, [ + 'clientId' => '', + 'clientSecret' => '', + 'enabled' => false, + ]); + } + + // ========================================================================= + // List OAuth2 providers + // ========================================================================= + + public function testListOAuth2Providers(): void + { + $response = $this->listOAuth2Providers(); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertArrayHasKey('total', $response['body']); + $this->assertArrayHasKey('providers', $response['body']); + $this->assertGreaterThan(0, $response['body']['total']); + $this->assertSame($response['body']['total'], \count($response['body']['providers'])); + } + + public function testListOAuth2ProvidersIncludesKnownProviders(): void + { + $response = $this->listOAuth2Providers(); + + $this->assertSame(200, $response['headers']['status-code']); + + $ids = \array_column($response['body']['providers'], '$id'); + + // Spot-check a representative cross-section of providers across all + // provider shapes (plain, multi-field, sandboxed, custom param names). + $expected = [ + 'github', + 'amazon', + 'apple', + 'auth0', + 'authentik', + 'gitlab', + 'oidc', + 'okta', + 'microsoft', + 'dropbox', + 'paypalSandbox', + 'kick', + ]; + + foreach ($expected as $providerId) { + $this->assertContains($providerId, $ids, "Missing provider {$providerId} in listOAuth2Providers response"); + } + } + + public function testListOAuth2ProvidersResponseShape(): void + { + $response = $this->listOAuth2Providers(); + + $this->assertSame(200, $response['headers']['status-code']); + + foreach ($response['body']['providers'] as $provider) { + $this->assertArrayHasKey('$id', $provider); + $this->assertArrayHasKey('enabled', $provider); + $this->assertIsString($provider['$id']); + $this->assertIsBool($provider['enabled']); + } + } + + public function testListOAuth2ProvidersClientSecretsNotExposed(): void + { + // Seed credentials so the list cannot trivially return empty values. + $this->updateOAuth2($this->plainProvider, [ + 'clientId' => 'amzn1.application-oa2-client.testListSeed', + 'clientSecret' => 'super-secret-must-not-leak', + 'enabled' => false, + ]); + + $response = $this->listOAuth2Providers(); + + $this->assertSame(200, $response['headers']['status-code']); + + $matched = false; + foreach ($response['body']['providers'] as $provider) { + if ($provider['$id'] !== $this->plainProvider) { + continue; + } + + $matched = true; + $this->assertSame('amzn1.application-oa2-client.testListSeed', $provider['clientId']); + $this->assertSame('', $provider['clientSecret']); + } + + $this->assertTrue($matched, 'List did not include the seeded provider.'); + } + + public function testListOAuth2ProvidersWithoutAuthentication(): void + { + $response = $this->listOAuth2Providers(authenticated: false); + + $this->assertSame(401, $response['headers']['status-code']); + } + + // ========================================================================= + // Get OAuth2 provider + // ========================================================================= + + public function testGetOAuth2Provider(): void + { + $response = $this->getOAuth2Provider('github'); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('github', $response['body']['$id']); + $this->assertArrayHasKey('enabled', $response['body']); + $this->assertArrayHasKey('clientId', $response['body']); + $this->assertArrayHasKey('clientSecret', $response['body']); + $this->assertSame('', $response['body']['clientSecret']); + } + + public function testGetOAuth2ProviderClientSecretWriteOnly(): void + { + $this->updateOAuth2($this->plainProvider, [ + 'clientId' => 'amzn1.application-oa2-client.getSecretCheck', + 'clientSecret' => 'must-never-be-returned', + 'enabled' => false, + ]); + + $response = $this->getOAuth2Provider($this->plainProvider); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('amzn1.application-oa2-client.getSecretCheck', $response['body']['clientId']); + $this->assertSame('', $response['body']['clientSecret']); + } + + public function testGetOAuth2ProviderMatchesListEntry(): void + { + $list = $this->listOAuth2Providers(); + $this->assertSame(200, $list['headers']['status-code']); + + $byId = []; + foreach ($list['body']['providers'] as $provider) { + $byId[$provider['$id']] = $provider; + } + + // Match GET against LIST for one provider per shape. + foreach (['github', 'amazon', 'dropbox', 'gitlab', 'apple', 'oidc', 'microsoft'] as $providerId) { + $get = $this->getOAuth2Provider($providerId); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertArrayHasKey($providerId, $byId, "{$providerId} missing from list"); + $this->assertSame($byId[$providerId], $get['body']); + } + } + + public function testGetOAuth2ProviderUnsupported(): void + { + $response = $this->getOAuth2Provider('not-a-real-provider'); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('project_provider_unsupported', $response['body']['type']); + } + + public function testGetOAuth2ProviderWithoutAuthentication(): void + { + $response = $this->getOAuth2Provider('github', authenticated: false); + + $this->assertSame(401, $response['headers']['status-code']); + } + + // ========================================================================= + // Update plain provider (Amazon — clientId + clientSecret, no extra fields) + // ========================================================================= + + public function testUpdateOAuth2Plain(): void + { + $response = $this->updateOAuth2($this->plainProvider, [ + 'clientId' => 'amzn1.application-oa2-client.test01', + 'clientSecret' => 'test-secret-01', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame($this->plainProvider, $response['body']['$id']); + $this->assertSame('amzn1.application-oa2-client.test01', $response['body']['clientId']); + $this->assertSame(false, $response['body']['enabled']); + } + + public function testUpdateOAuth2PlainEnable(): void + { + // Amazon has no verifyCredentials() hook, so enabling with arbitrary + // credentials succeeds without making a real network call. + $response = $this->updateOAuth2($this->plainProvider, [ + 'clientId' => 'amzn1.application-oa2-client.test02', + 'clientSecret' => 'test-secret-02', + 'enabled' => true, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(true, $response['body']['enabled']); + } + + public function testUpdateOAuth2PlainDisable(): void + { + $this->updateOAuth2($this->plainProvider, [ + 'clientId' => 'amzn1.application-oa2-client.test03', + 'clientSecret' => 'test-secret-03', + 'enabled' => true, + ]); + + $response = $this->updateOAuth2($this->plainProvider, [ + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(false, $response['body']['enabled']); + // Credentials persist across an enabled toggle. + $this->assertSame('amzn1.application-oa2-client.test03', $response['body']['clientId']); + } + + public function testUpdateOAuth2PlainPartial(): void + { + // Seed both credentials. + $this->updateOAuth2($this->plainProvider, [ + 'clientId' => 'seed-client-id', + 'clientSecret' => 'seed-secret', + 'enabled' => false, + ]); + + // Patch only clientId. + $response = $this->updateOAuth2($this->plainProvider, [ + 'clientId' => 'updated-client-id', + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('updated-client-id', $response['body']['clientId']); + + // Read back through GET to confirm the secret is still set internally + // (write-only, so we cannot inspect the value, but enabling should still + // succeed because the secret remains non-empty). + $enable = $this->updateOAuth2($this->plainProvider, [ + 'enabled' => true, + ]); + $this->assertSame(200, $enable['headers']['status-code']); + $this->assertSame(true, $enable['body']['enabled']); + } + + public function testUpdateOAuth2PlainEnableRequiresCredentials(): void + { + // Start from a clean state with no credentials. + $this->updateOAuth2($this->plainProvider, [ + 'clientId' => '', + 'clientSecret' => '', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2($this->plainProvider, [ + 'enabled' => true, + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + } + + public function testUpdateOAuth2PlainEnabledOmittedDoesNotThrow(): void + { + // With enabled omitted (null) and no credentials, the silent-validation + // branch must not surface as an error. + $this->updateOAuth2($this->plainProvider, [ + 'clientId' => '', + 'clientSecret' => '', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2($this->plainProvider, [ + 'clientId' => 'partial-only', + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(false, $response['body']['enabled']); + $this->assertSame('partial-only', $response['body']['clientId']); + } + + public function testUpdateOAuth2PlainResponseModel(): void + { + $response = $this->updateOAuth2($this->plainProvider, [ + 'clientId' => 'amzn1.application-oa2-client.modelCheck', + 'clientSecret' => 'model-check-secret', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertArrayHasKey('$id', $response['body']); + $this->assertArrayHasKey('enabled', $response['body']); + $this->assertArrayHasKey('clientId', $response['body']); + $this->assertArrayHasKey('clientSecret', $response['body']); + } + + public function testUpdateOAuth2WithoutAuthentication(): void + { + $response = $this->updateOAuth2($this->plainProvider, [ + 'clientId' => 'no-auth', + 'clientSecret' => 'no-auth', + 'enabled' => false, + ], authenticated: false); + + $this->assertSame(401, $response['headers']['status-code']); + } + + public function testUpdateOAuth2UnknownProvider(): void + { + // Each Update endpoint is registered at a fixed `/oauth2/{providerId}` + // path, so an unknown provider does not match any route → 404. + $response = $this->updateOAuth2('not-a-real-provider', [ + 'clientId' => 'whatever', + 'clientSecret' => 'whatever', + 'enabled' => false, + ]); + + $this->assertSame(404, $response['headers']['status-code']); + } + + public function testUpdateOAuth2InvalidEnabled(): void + { + $response = $this->updateOAuth2($this->plainProvider, [ + 'enabled' => 'not-a-boolean', + ]); + + $this->assertSame(400, $response['headers']['status-code']); + } + + // ========================================================================= + // Update GitHub (verifyCredentials makes a real call to GitHub on enable) + // ========================================================================= + + public function testUpdateOAuth2GitHubInvalidCredentialsRejected(): void + { + // GitHub is the only provider with a real verifyCredentials() hook. + // Enabling with bogus credentials must surface a 400 from the wrapping + // exception, not silently succeed. + $response = $this->updateOAuth2('github', [ + 'clientId' => 'fake-client-id-' . \uniqid(), + 'clientSecret' => 'fake-client-secret', + 'enabled' => true, + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + + // Cleanup: ensure it's left disabled. + $this->updateOAuth2('github', [ + 'clientId' => '', + 'clientSecret' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2GitHubInvalidCredentialsSilentWhenNotEnabling(): void + { + // When `enabled` is omitted, verifyCredentials() failure is swallowed. + // The provider remains disabled but the request succeeds. + $response = $this->updateOAuth2('github', [ + 'clientId' => 'still-fake-' . \uniqid(), + 'clientSecret' => 'still-fake-secret', + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame(false, $response['body']['enabled']); + + // Cleanup + $this->updateOAuth2('github', [ + 'clientId' => '', + 'clientSecret' => '', + 'enabled' => false, + ]); + } + + // ========================================================================= + // Update Apple (serviceId + keyId + teamId + p8File) + // ========================================================================= + + public function testUpdateOAuth2Apple(): void + { + $response = $this->updateOAuth2('apple', [ + 'serviceId' => 'ip.appwrite.app.web', + 'keyId' => 'P4000000N8', + 'teamId' => 'D4000000R6', + 'p8File' => '-----BEGIN PRIVATE KEY-----TEST-----END PRIVATE KEY-----', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('apple', $response['body']['$id']); + $this->assertSame('ip.appwrite.app.web', $response['body']['serviceId']); + $this->assertSame('P4000000N8', $response['body']['keyId']); + $this->assertSame('D4000000R6', $response['body']['teamId']); + $this->assertSame(false, $response['body']['enabled']); + + // Cleanup + $this->updateOAuth2('apple', [ + 'serviceId' => '', + 'keyId' => '', + 'teamId' => '', + 'p8File' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2ApplePartial(): void + { + // Seed all four fields. + $this->updateOAuth2('apple', [ + 'serviceId' => 'ip.appwrite.app.seed', + 'keyId' => 'KEYSEED01', + 'teamId' => 'TEAMSEED01', + 'p8File' => '-----BEGIN PRIVATE KEY-----SEED-----END PRIVATE KEY-----', + 'enabled' => false, + ]); + + // Patch only `keyId` — others must be preserved. + $response = $this->updateOAuth2('apple', [ + 'keyId' => 'KEYUPDATED', + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('KEYUPDATED', $response['body']['keyId']); + $this->assertSame('TEAMSEED01', $response['body']['teamId']); + $this->assertSame('ip.appwrite.app.seed', $response['body']['serviceId']); + + // Cleanup + $this->updateOAuth2('apple', [ + 'serviceId' => '', + 'keyId' => '', + 'teamId' => '', + 'p8File' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2AppleResponseModel(): void + { + $response = $this->updateOAuth2('apple', [ + 'serviceId' => 'ip.appwrite.app.shape', + 'keyId' => 'SHAPEKEY01', + 'teamId' => 'SHAPETEAM', + 'p8File' => '-----BEGIN PRIVATE KEY-----SHAPE-----END PRIVATE KEY-----', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertArrayHasKey('$id', $response['body']); + $this->assertArrayHasKey('enabled', $response['body']); + $this->assertArrayHasKey('serviceId', $response['body']); + $this->assertArrayHasKey('keyId', $response['body']); + $this->assertArrayHasKey('teamId', $response['body']); + $this->assertArrayHasKey('p8File', $response['body']); + // Apple has no clientId/clientSecret in the response model. + $this->assertArrayNotHasKey('clientId', $response['body']); + $this->assertArrayNotHasKey('clientSecret', $response['body']); + + // Cleanup + $this->updateOAuth2('apple', [ + 'serviceId' => '', + 'keyId' => '', + 'teamId' => '', + 'p8File' => '', + 'enabled' => false, + ]); + } + + public function testGetOAuth2AppleSecretsWriteOnly(): void + { + $this->updateOAuth2('apple', [ + 'serviceId' => 'ip.appwrite.app.read', + 'keyId' => 'KEYREAD', + 'teamId' => 'TEAMREAD', + 'p8File' => '-----BEGIN PRIVATE KEY-----READ-----END PRIVATE KEY-----', + 'enabled' => false, + ]); + + $response = $this->getOAuth2Provider('apple'); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('ip.appwrite.app.read', $response['body']['serviceId']); + // All three secret-bearing fields must be hidden on read. + $this->assertSame('', $response['body']['keyId']); + $this->assertSame('', $response['body']['teamId']); + $this->assertSame('', $response['body']['p8File']); + + // Cleanup + $this->updateOAuth2('apple', [ + 'serviceId' => '', + 'keyId' => '', + 'teamId' => '', + 'p8File' => '', + 'enabled' => false, + ]); + } + + // ========================================================================= + // Update Auth0 (clientId + clientSecret + optional endpoint) + // ========================================================================= + + public function testUpdateOAuth2Auth0(): void + { + $response = $this->updateOAuth2('auth0', [ + 'clientId' => 'OaOkIA000000000000000000005KLSYq', + 'clientSecret' => 'auth0-test-secret', + 'endpoint' => 'example.us.auth0.com', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('auth0', $response['body']['$id']); + $this->assertSame('OaOkIA000000000000000000005KLSYq', $response['body']['clientId']); + $this->assertSame('example.us.auth0.com', $response['body']['endpoint']); + + // Cleanup + $this->updateOAuth2('auth0', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2Auth0PartialEndpoint(): void + { + // Seed clientSecret + endpoint. + $this->updateOAuth2('auth0', [ + 'clientId' => 'auth0-seed-client', + 'clientSecret' => 'auth0-seed-secret', + 'endpoint' => 'seed.us.auth0.com', + 'enabled' => false, + ]); + + // Update only endpoint. + $response = $this->updateOAuth2('auth0', [ + 'endpoint' => 'updated.us.auth0.com', + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('updated.us.auth0.com', $response['body']['endpoint']); + // clientId is unchanged on top-level provider state. + $this->assertSame('auth0-seed-client', $response['body']['clientId']); + + // Cleanup + $this->updateOAuth2('auth0', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => '', + 'enabled' => false, + ]); + } + + // ========================================================================= + // Update Authentik (clientId + clientSecret + REQUIRED endpoint) + // ========================================================================= + + public function testUpdateOAuth2AuthentikRequiresEndpoint(): void + { + // The `endpoint` param is required (Text(min=1)); omitting → 400. + $response = $this->updateOAuth2('authentik', [ + 'clientId' => 'whatever', + 'clientSecret' => 'whatever', + ]); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateOAuth2Authentik(): void + { + $response = $this->updateOAuth2('authentik', [ + 'clientId' => 'dTKOPa0000000000000000000000000000e7G8hv', + 'clientSecret' => 'authentik-secret', + 'endpoint' => 'example.authentik.com', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('authentik', $response['body']['$id']); + $this->assertSame('dTKOPa0000000000000000000000000000e7G8hv', $response['body']['clientId']); + $this->assertSame('example.authentik.com', $response['body']['endpoint']); + + // Cleanup + $this->updateOAuth2('authentik', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => 'cleanup.authentik.com', + 'enabled' => false, + ]); + } + + // ========================================================================= + // Update Microsoft (applicationId + applicationSecret + REQUIRED tenant) + // ========================================================================= + + public function testUpdateOAuth2MicrosoftRequiresTenant(): void + { + $response = $this->updateOAuth2('microsoft', [ + 'applicationId' => 'whatever', + 'applicationSecret' => 'whatever', + ]); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateOAuth2Microsoft(): void + { + $response = $this->updateOAuth2('microsoft', [ + 'applicationId' => '00001111-aaaa-2222-bbbb-3333cccc4444', + 'applicationSecret' => 'A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u', + 'tenant' => 'common', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('microsoft', $response['body']['$id']); + $this->assertSame('00001111-aaaa-2222-bbbb-3333cccc4444', $response['body']['applicationId']); + $this->assertSame('common', $response['body']['tenant']); + // Custom param names: applicationId/applicationSecret, not clientId/clientSecret. + $this->assertArrayNotHasKey('clientId', $response['body']); + $this->assertArrayNotHasKey('clientSecret', $response['body']); + + // Cleanup + $this->updateOAuth2('microsoft', [ + 'applicationId' => '', + 'applicationSecret' => '', + 'tenant' => 'common', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2MicrosoftPartialPreservesSecret(): void + { + // Seed full credentials. + $this->updateOAuth2('microsoft', [ + 'applicationId' => 'seed-app-id', + 'applicationSecret' => 'seed-app-secret', + 'tenant' => 'common', + 'enabled' => false, + ]); + + // Patch with only `tenant` (it's required on every call) and a new + // applicationId, leaving applicationSecret omitted. The stored secret + // must not be wiped. + $response = $this->updateOAuth2('microsoft', [ + 'applicationId' => 'updated-app-id', + 'tenant' => 'organizations', + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('updated-app-id', $response['body']['applicationId']); + $this->assertSame('organizations', $response['body']['tenant']); + + // Cleanup + $this->updateOAuth2('microsoft', [ + 'applicationId' => '', + 'applicationSecret' => '', + 'tenant' => 'common', + 'enabled' => false, + ]); + } + + // ========================================================================= + // Update Gitlab (applicationId + secret + optional endpoint, custom names) + // ========================================================================= + + public function testUpdateOAuth2Gitlab(): void + { + $response = $this->updateOAuth2('gitlab', [ + 'applicationId' => 'd41ffe0000000000000000000000000000000000000000000000000000d5e252', + 'secret' => 'gloas-838cfa00', + 'endpoint' => 'https://gitlab.example.com', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('gitlab', $response['body']['$id']); + $this->assertSame('d41ffe0000000000000000000000000000000000000000000000000000d5e252', $response['body']['applicationId']); + $this->assertSame('https://gitlab.example.com', $response['body']['endpoint']); + // Custom names — the response model exposes `applicationId`/`secret`. + $this->assertArrayNotHasKey('clientId', $response['body']); + $this->assertArrayNotHasKey('clientSecret', $response['body']); + + // Cleanup + $this->updateOAuth2('gitlab', [ + 'applicationId' => '', + 'secret' => '', + 'endpoint' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2GitlabInvalidEndpoint(): void + { + $response = $this->updateOAuth2('gitlab', [ + 'applicationId' => 'whatever', + 'secret' => 'whatever', + 'endpoint' => 'not a url', + ]); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateOAuth2GitlabPartialEndpoint(): void + { + $this->updateOAuth2('gitlab', [ + 'applicationId' => 'gitlab-seed-app', + 'secret' => 'gitlab-seed-secret', + 'endpoint' => 'https://seed.gitlab.com', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('gitlab', [ + 'endpoint' => 'https://updated.gitlab.com', + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('https://updated.gitlab.com', $response['body']['endpoint']); + $this->assertSame('gitlab-seed-app', $response['body']['applicationId']); + + // Cleanup + $this->updateOAuth2('gitlab', [ + 'applicationId' => '', + 'secret' => '', + 'endpoint' => '', + 'enabled' => false, + ]); + } + + // ========================================================================= + // Update OIDC (clientId + secret + wellKnownURL or 3 discovery URLs) + // ========================================================================= + + public function testUpdateOAuth2OidcWithWellKnown(): void + { + $response = $this->updateOAuth2('oidc', [ + 'clientId' => 'oidc-client', + 'clientSecret' => 'oidc-secret', + 'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('https://idp.example.com/.well-known/openid-configuration', $response['body']['wellKnownURL']); + $this->assertArrayHasKey('authorizationURL', $response['body']); + $this->assertArrayHasKey('tokenUrl', $response['body']); + $this->assertArrayHasKey('userInfoUrl', $response['body']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2OidcWithDiscoveryURLs(): void + { + $response = $this->updateOAuth2('oidc', [ + 'clientId' => 'oidc-discovery', + 'clientSecret' => 'oidc-discovery-secret', + 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', + 'tokenUrl' => 'https://idp.example.com/oauth2/token', + 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('https://idp.example.com/oauth2/authorize', $response['body']['authorizationURL']); + $this->assertSame('https://idp.example.com/oauth2/token', $response['body']['tokenUrl']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $response['body']['userInfoUrl']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2OidcEnableMissingURLs(): void + { + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('oidc', [ + 'clientId' => 'oidc-no-urls', + 'clientSecret' => 'oidc-no-urls', + 'enabled' => true, + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2OidcEnablePartialDiscoveryFails(): void + { + // Only authorization+token, missing userInfo — must fail to enable. + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('oidc', [ + 'clientId' => 'oidc-partial', + 'clientSecret' => 'oidc-partial-secret', + 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', + 'tokenUrl' => 'https://idp.example.com/oauth2/token', + 'enabled' => true, + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + } + + // ========================================================================= + // Update Okta (clientId + clientSecret + optional domain/authServer) + // ========================================================================= + + public function testUpdateOAuth2Okta(): void + { + $response = $this->updateOAuth2('okta', [ + 'clientId' => '0oa00000000000000698', + 'clientSecret' => 'okta-secret', + 'domain' => 'trial-6400025.okta.com', + 'authorizationServerId' => 'aus000000000000000h7z', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('okta', $response['body']['$id']); + $this->assertSame('0oa00000000000000698', $response['body']['clientId']); + $this->assertSame('trial-6400025.okta.com', $response['body']['domain']); + $this->assertSame('aus000000000000000h7z', $response['body']['authorizationServerId']); + + // Cleanup + $this->updateOAuth2('okta', [ + 'clientId' => '', + 'clientSecret' => '', + 'domain' => '', + 'authorizationServerId' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2OktaInvalidDomain(): void + { + $response = $this->updateOAuth2('okta', [ + 'clientId' => 'whatever', + 'clientSecret' => 'whatever', + 'domain' => 'https://trial-6400025.okta.com/', + ]); + + $this->assertSame(400, $response['headers']['status-code']); + } + + public function testUpdateOAuth2OktaEnableRequiresDomain(): void + { + $this->updateOAuth2('okta', [ + 'clientId' => '', + 'clientSecret' => '', + 'domain' => '', + 'authorizationServerId' => '', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('okta', [ + 'clientId' => 'okta-no-domain', + 'clientSecret' => 'okta-no-domain-secret', + 'enabled' => true, + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + + // Cleanup + $this->updateOAuth2('okta', [ + 'clientId' => '', + 'clientSecret' => '', + 'enabled' => false, + ]); + } + + // ========================================================================= + // Update Dropbox (custom param names: appKey + appSecret) + // ========================================================================= + + public function testUpdateOAuth2DropboxFieldNames(): void + { + $response = $this->updateOAuth2('dropbox', [ + 'appKey' => 'jl000000000009t', + 'appSecret' => 'g200000000000vw', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('dropbox', $response['body']['$id']); + $this->assertSame('jl000000000009t', $response['body']['appKey']); + $this->assertArrayHasKey('appSecret', $response['body']); + $this->assertArrayNotHasKey('clientId', $response['body']); + $this->assertArrayNotHasKey('clientSecret', $response['body']); + + // GET enforces write-only on the secret regardless of the custom name. + $get = $this->getOAuth2Provider('dropbox'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertSame('jl000000000009t', $get['body']['appKey']); + $this->assertSame('', $get['body']['appSecret']); + + // Cleanup + $this->updateOAuth2('dropbox', [ + 'appKey' => '', + 'appSecret' => '', + 'enabled' => false, + ]); + } + + // ========================================================================= + // Update Paypal Sandbox (inherits from Paypal — independent provider ID) + // ========================================================================= + + public function testUpdateOAuth2PaypalSandbox(): void + { + $response = $this->updateOAuth2('paypalSandbox', [ + 'clientId' => 'paypal-sandbox-client', + 'clientSecret' => 'paypal-sandbox-secret', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('paypalSandbox', $response['body']['$id']); + $this->assertSame('paypal-sandbox-client', $response['body']['clientId']); + + // Sandbox is independent of the regular paypal entry. + $regular = $this->getOAuth2Provider('paypal'); + $this->assertSame(200, $regular['headers']['status-code']); + $this->assertSame('paypal', $regular['body']['$id']); + $this->assertNotSame('paypal-sandbox-client', $regular['body']['clientId']); + + // Cleanup + $this->updateOAuth2('paypalSandbox', [ + 'clientId' => '', + 'clientSecret' => '', + 'enabled' => false, + ]); + } + + // ========================================================================= + // Helpers + // ========================================================================= + + /** + * @param array $params + */ + protected function updateOAuth2(string $provider, array $params, bool $authenticated = true): mixed + { + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]; + + if ($authenticated) { + $headers = \array_merge($headers, $this->getHeaders()); + } + + return $this->client->call( + Client::METHOD_PATCH, + '/project/oauth2/' . $provider, + $headers, + $params, + ); + } + + protected function getOAuth2Provider(string $provider, bool $authenticated = true): mixed + { + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]; + + if ($authenticated) { + $headers = \array_merge($headers, $this->getHeaders()); + } + + return $this->client->call( + Client::METHOD_GET, + '/project/oauth2/' . $provider, + $headers, + ); + } + + protected function listOAuth2Providers(bool $authenticated = true): mixed + { + $headers = [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]; + + if ($authenticated) { + $headers = \array_merge($headers, $this->getHeaders()); + } + + return $this->client->call( + Client::METHOD_GET, + '/project/oauth2', + $headers, + ); + } } From 4ba413fcc0eb2528c67d19a2ad04b31dbf11dff1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 16:50:14 +0200 Subject: [PATCH 34/51] Fix bugs when implementing tests --- .../Project/Http/Project/OAuth2/Apple/Update.php | 10 +++++++--- .../Project/Http/Project/OAuth2/Auth0/Update.php | 10 +++++++--- .../Project/Http/Project/OAuth2/Authentik/Update.php | 10 +++++++--- .../Modules/Project/Http/Project/OAuth2/Base.php | 11 ++++++++--- .../Project/Http/Project/OAuth2/Gitlab/Update.php | 10 +++++++--- .../Project/Http/Project/OAuth2/Microsoft/Update.php | 10 +++++++--- .../Project/Http/Project/OAuth2/Oidc/Update.php | 10 +++++++--- .../Project/Http/Project/OAuth2/Okta/Update.php | 10 +++++++--- .../Platform/Modules/Project/Services/Http.php | 2 ++ src/Appwrite/Utopia/Response/Model/OAuth2Amazon.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Apple.php | 10 ++++++++++ src/Appwrite/Utopia/Response/Model/OAuth2Auth0.php | 4 ++++ .../Utopia/Response/Model/OAuth2Authentik.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Autodesk.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Base.php | 6 ++++++ .../Utopia/Response/Model/OAuth2Bitbucket.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Bitly.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Box.php | 4 ++++ .../Utopia/Response/Model/OAuth2Dailymotion.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Discord.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Disqus.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Dropbox.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Etsy.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Facebook.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Figma.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Gitlab.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Google.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Kick.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Linkedin.php | 4 ++++ .../Utopia/Response/Model/OAuth2Microsoft.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Notion.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Okta.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Paypal.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Podio.php | 4 ++++ .../Utopia/Response/Model/OAuth2Salesforce.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Slack.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Spotify.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Stripe.php | 4 ++++ .../Utopia/Response/Model/OAuth2Tradeshift.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Twitch.php | 4 ++++ .../Utopia/Response/Model/OAuth2WordPress.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2X.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Yahoo.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Yandex.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Zoho.php | 4 ++++ src/Appwrite/Utopia/Response/Model/OAuth2Zoom.php | 4 ++++ 48 files changed, 223 insertions(+), 24 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php index 4f8437ce8d..79a30e02d4 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Apple; use Appwrite\Auth\OAuth2\Apple; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base; use Appwrite\SDK\AuthType; @@ -71,8 +72,8 @@ class Update extends Base ->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('event', 'oauth2.[providerId].update') + ->label('audits.event', 'project.oauth2.[providerId].update') ->label('audits.resource', 'project.oauth2/{response.$id}') ->label('sdk', new Method( namespace: 'project', @@ -96,6 +97,7 @@ class Update extends Base ->inject('dbForPlatform') ->inject('project') ->inject('authorization') + ->inject('queueForEvents') ->callback($this->handle(...)); } @@ -130,9 +132,11 @@ class Update extends Base Response $response, Database $dbForPlatform, Document $project, - Authorization $authorization + Authorization $authorization, + QueueEvent $queueForEvents ): void { $providerId = static::getProviderId(); + $queueForEvents->setParam('providerId', $providerId); // The secret is stored as JSON `{"p8": "...", "keyID": "...", "teamID": "..."}` // to match the shape Apple's OAuth2 adapter expects in getAppSecret(). diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php index 1bbdd02a0d..4cb314af13 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Auth0; use Appwrite\Auth\OAuth2\Auth0; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base; use Appwrite\SDK\AuthType; @@ -64,8 +65,8 @@ class Update extends Base ->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('event', 'oauth2.[providerId].update') + ->label('audits.event', 'project.oauth2.[providerId].update') ->label('audits.resource', 'project.oauth2/{response.$id}') ->label('sdk', new Method( namespace: 'project', @@ -88,6 +89,7 @@ class Update extends Base ->inject('dbForPlatform') ->inject('project') ->inject('authorization') + ->inject('queueForEvents') ->callback($this->handle(...)); } @@ -119,9 +121,11 @@ class Update extends Base Response $response, Database $dbForPlatform, Document $project, - Authorization $authorization + Authorization $authorization, + QueueEvent $queueForEvents ): void { $providerId = static::getProviderId(); + $queueForEvents->setParam('providerId', $providerId); // The secret is stored as JSON `{"clientSecret": "...", "auth0Domain": "..."}` // to match the shape Auth0's OAuth2 adapter expects (getAuth0Domain()). diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php index 62e314053a..834a68597a 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Authentik; use Appwrite\Auth\OAuth2\Authentik; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base; use Appwrite\SDK\AuthType; @@ -64,8 +65,8 @@ class Update extends Base ->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('event', 'oauth2.[providerId].update') + ->label('audits.event', 'project.oauth2.[providerId].update') ->label('audits.resource', 'project.oauth2/{response.$id}') ->label('sdk', new Method( namespace: 'project', @@ -88,6 +89,7 @@ class Update extends Base ->inject('dbForPlatform') ->inject('project') ->inject('authorization') + ->inject('queueForEvents') ->callback($this->handle(...)); } @@ -119,9 +121,11 @@ class Update extends Base Response $response, Database $dbForPlatform, Document $project, - Authorization $authorization + Authorization $authorization, + QueueEvent $queueForEvents ): void { $providerId = static::getProviderId(); + $queueForEvents->setParam('providerId', $providerId); // The secret is stored as JSON `{"clientSecret": "...", "authentikDomain": "..."}` // to match the shape Authentik's OAuth2 adapter expects (getAuthentikDomain()). diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php index ddaac7c602..50531d647f 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php @@ -2,6 +2,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Action; use Appwrite\SDK\AuthType; @@ -111,8 +112,8 @@ abstract class Base extends Action ->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('event', 'oauth2.[providerId].update') + ->label('audits.event', 'project.oauth2.[providerId].update') ->label('audits.resource', 'project.oauth2/{response.$id}') ->label('sdk', new Method( namespace: 'project', @@ -134,6 +135,7 @@ abstract class Base extends Action ->inject('dbForPlatform') ->inject('project') ->inject('authorization') + ->inject('queueForEvents') ->callback($this->action(...)); } @@ -304,13 +306,16 @@ abstract class Base extends Action Response $response, Database $dbForPlatform, Document $project, - Authorization $authorization + Authorization $authorization, + QueueEvent $queueForEvents ): void { $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $clientSecret, $enabled); $providerId = static::getProviderId(); $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $queueForEvents->setParam('providerId', $providerId); + $response->dynamic(new Document([ '$id' => $providerId, 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php index 8d4f4e88da..a727f3f3a4 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Gitlab; use Appwrite\Auth\OAuth2\Gitlab; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base; use Appwrite\SDK\AuthType; @@ -75,8 +76,8 @@ class Update extends Base ->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('event', 'oauth2.[providerId].update') + ->label('audits.event', 'project.oauth2.[providerId].update') ->label('audits.resource', 'project.oauth2/{response.$id}') ->label('sdk', new Method( namespace: 'project', @@ -99,6 +100,7 @@ class Update extends Base ->inject('dbForPlatform') ->inject('project') ->inject('authorization') + ->inject('queueForEvents') ->callback($this->handle(...)); } @@ -130,9 +132,11 @@ class Update extends Base Response $response, Database $dbForPlatform, Document $project, - Authorization $authorization + Authorization $authorization, + QueueEvent $queueForEvents ): void { $providerId = static::getProviderId(); + $queueForEvents->setParam('providerId', $providerId); // The secret is stored as JSON `{"clientSecret": "...", "endpoint": "..."}` // so that the Gitlab OAuth2 adapter can extract the endpoint via getEndpoint(). diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php index 60479cf5f5..894631fbaa 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Microsoft; use Appwrite\Auth\OAuth2\Microsoft; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base; use Appwrite\SDK\AuthType; @@ -74,8 +75,8 @@ class Update extends Base ->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('event', 'oauth2.[providerId].update') + ->label('audits.event', 'project.oauth2.[providerId].update') ->label('audits.resource', 'project.oauth2/{response.$id}') ->label('sdk', new Method( namespace: 'project', @@ -98,6 +99,7 @@ class Update extends Base ->inject('dbForPlatform') ->inject('project') ->inject('authorization') + ->inject('queueForEvents') ->callback($this->handle(...)); } @@ -129,9 +131,11 @@ class Update extends Base Response $response, Database $dbForPlatform, Document $project, - Authorization $authorization + Authorization $authorization, + QueueEvent $queueForEvents ): void { $providerId = static::getProviderId(); + $queueForEvents->setParam('providerId', $providerId); // The secret is stored as JSON `{"clientSecret": "...", "tenantID": "..."}` // to match the shape Microsoft's OAuth2 adapter expects (getTenantID()). diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php index d849e18efd..f950c78b13 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Oidc; use Appwrite\Auth\OAuth2\Oidc; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base; @@ -66,8 +67,8 @@ class Update extends Base ->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('event', 'oauth2.[providerId].update') + ->label('audits.event', 'project.oauth2.[providerId].update') ->label('audits.resource', 'project.oauth2/{response.$id}') ->label('sdk', new Method( namespace: 'project', @@ -93,6 +94,7 @@ class Update extends Base ->inject('dbForPlatform') ->inject('project') ->inject('authorization') + ->inject('queueForEvents') ->callback($this->handle(...)); } @@ -138,9 +140,11 @@ class Update extends Base Response $response, Database $dbForPlatform, Document $project, - Authorization $authorization + Authorization $authorization, + QueueEvent $queueForEvents ): void { $providerId = static::getProviderId(); + $queueForEvents->setParam('providerId', $providerId); // The secret is stored as JSON // `{"clientSecret": "...", "wellKnownEndpoint": "...", "authorizationEndpoint": "...", "tokenEndpoint": "...", "userInfoEndpoint": "..."}` diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php index 47d6cb2add..1aef7684be 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php @@ -3,6 +3,7 @@ namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Okta; use Appwrite\Auth\OAuth2\Okta; +use Appwrite\Event\Event as QueueEvent; use Appwrite\Extend\Exception; use Appwrite\Platform\Action; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base; @@ -66,8 +67,8 @@ class Update extends Base ->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('event', 'oauth2.[providerId].update') + ->label('audits.event', 'project.oauth2.[providerId].update') ->label('audits.resource', 'project.oauth2/{response.$id}') ->label('sdk', new Method( namespace: 'project', @@ -91,6 +92,7 @@ class Update extends Base ->inject('dbForPlatform') ->inject('project') ->inject('authorization') + ->inject('queueForEvents') ->callback($this->handle(...)); } @@ -125,9 +127,11 @@ class Update extends Base Response $response, Database $dbForPlatform, Document $project, - Authorization $authorization + Authorization $authorization, + QueueEvent $queueForEvents ): void { $providerId = static::getProviderId(); + $queueForEvents->setParam('providerId', $providerId); // The secret is stored as JSON `{"clientSecret": "...", "oktaDomain": "...", "authorizationServerId": "..."}` // to match the shape Okta's OAuth2 adapter expects. diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index 8a330ca041..d6ff3c4925 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -17,6 +17,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\Get as GetMockPhone use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\Update as UpdateMockPhone; use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\XList as ListMockPhones; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Amazon\Update as UpdateOAuth2Amazon; +use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Apple\Update as UpdateOAuth2Apple; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Auth0\Update as UpdateOAuth2Auth0; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Authentik\Update as UpdateOAuth2Authentik; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Autodesk\Update as UpdateOAuth2Autodesk; @@ -212,6 +213,7 @@ class Http extends Service $this->addAction(UpdateOAuth2Oidc::getName(), new UpdateOAuth2Oidc()); $this->addAction(UpdateOAuth2Okta::getName(), new UpdateOAuth2Okta()); $this->addAction(UpdateOAuth2Kick::getName(), new UpdateOAuth2Kick()); + $this->addAction(UpdateOAuth2Apple::getName(), new UpdateOAuth2Apple()); $this->addAction(UpdateOAuth2Microsoft::getName(), new UpdateOAuth2Microsoft()); } } diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Amazon.php b/src/Appwrite/Utopia/Response/Model/OAuth2Amazon.php index 33708374cc..f6c935648d 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Amazon.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Amazon.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Amazon extends OAuth2Base { + public array $conditions = [ + '$id' => 'amazon', + ]; + public function getProviderLabel(): string { return 'Amazon'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Apple.php b/src/Appwrite/Utopia/Response/Model/OAuth2Apple.php index 080925e6d8..075494b8ef 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Apple.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Apple.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Apple extends OAuth2Base { + public array $conditions = [ + '$id' => 'apple', + ]; + public function getProviderLabel(): string { return 'Apple'; @@ -39,6 +43,12 @@ class OAuth2Apple extends OAuth2Base // contents, Key ID, Team ID) instead of a single clientSecret, so the // rules are defined manually rather than delegating to OAuth2Base. $this + ->addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'OAuth2 provider ID.', + 'default' => '', + 'example' => 'apple', + ]) ->addRule('enabled', [ 'type' => self::TYPE_BOOLEAN, 'description' => 'OAuth2 provider is active and can be used to create sessions.', diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Auth0.php b/src/Appwrite/Utopia/Response/Model/OAuth2Auth0.php index 2f1893f4d5..6e83b1b05b 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Auth0.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Auth0.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Auth0 extends OAuth2Base { + public array $conditions = [ + '$id' => 'auth0', + ]; + public function getProviderLabel(): string { return 'Auth0'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Authentik.php b/src/Appwrite/Utopia/Response/Model/OAuth2Authentik.php index 4e67e1f4fe..db192ea24b 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Authentik.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Authentik.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Authentik extends OAuth2Base { + public array $conditions = [ + '$id' => 'authentik', + ]; + public function getProviderLabel(): string { return 'Authentik'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Autodesk.php b/src/Appwrite/Utopia/Response/Model/OAuth2Autodesk.php index 6f55b5d475..3317f15bec 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Autodesk.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Autodesk.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Autodesk extends OAuth2Base { + public array $conditions = [ + '$id' => 'autodesk', + ]; + public function getProviderLabel(): string { return 'Autodesk'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Base.php b/src/Appwrite/Utopia/Response/Model/OAuth2Base.php index 8eb8d0f4cb..058afc0fa1 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Base.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Base.php @@ -97,6 +97,12 @@ abstract class OAuth2Base extends Model public function __construct() { $this + ->addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'OAuth2 provider ID.', + 'default' => '', + 'example' => 'github', + ]) ->addRule('enabled', [ 'type' => self::TYPE_BOOLEAN, 'description' => 'OAuth2 provider is active and can be used to create sessions.', diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Bitbucket.php b/src/Appwrite/Utopia/Response/Model/OAuth2Bitbucket.php index 3465cb6cd7..870cd0bda3 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Bitbucket.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Bitbucket.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Bitbucket extends OAuth2Base { + public array $conditions = [ + '$id' => 'bitbucket', + ]; + public function getProviderLabel(): string { return 'Bitbucket'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Bitly.php b/src/Appwrite/Utopia/Response/Model/OAuth2Bitly.php index e32d089898..6a27176d3d 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Bitly.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Bitly.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Bitly extends OAuth2Base { + public array $conditions = [ + '$id' => 'bitly', + ]; + public function getProviderLabel(): string { return 'Bitly'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Box.php b/src/Appwrite/Utopia/Response/Model/OAuth2Box.php index 6c23c0d3ad..9bbfd6021f 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Box.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Box.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Box extends OAuth2Base { + public array $conditions = [ + '$id' => 'box', + ]; + public function getProviderLabel(): string { return 'Box'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Dailymotion.php b/src/Appwrite/Utopia/Response/Model/OAuth2Dailymotion.php index 0e149c986c..6c3d0eba95 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Dailymotion.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Dailymotion.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Dailymotion extends OAuth2Base { + public array $conditions = [ + '$id' => 'dailymotion', + ]; + public function getProviderLabel(): string { return 'Dailymotion'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Discord.php b/src/Appwrite/Utopia/Response/Model/OAuth2Discord.php index da7c4873b5..6ac72ad8e4 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Discord.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Discord.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Discord extends OAuth2Base { + public array $conditions = [ + '$id' => 'discord', + ]; + public function getProviderLabel(): string { return 'Discord'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Disqus.php b/src/Appwrite/Utopia/Response/Model/OAuth2Disqus.php index dbdc973b65..bec78ed189 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Disqus.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Disqus.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Disqus extends OAuth2Base { + public array $conditions = [ + '$id' => 'disqus', + ]; + public function getProviderLabel(): string { return 'Disqus'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Dropbox.php b/src/Appwrite/Utopia/Response/Model/OAuth2Dropbox.php index 4924db1397..db7285fd47 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Dropbox.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Dropbox.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Dropbox extends OAuth2Base { + public array $conditions = [ + '$id' => 'dropbox', + ]; + public function getProviderLabel(): string { return 'Dropbox'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Etsy.php b/src/Appwrite/Utopia/Response/Model/OAuth2Etsy.php index f80cce7cf1..be12e4c51c 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Etsy.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Etsy.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Etsy extends OAuth2Base { + public array $conditions = [ + '$id' => 'etsy', + ]; + public function getProviderLabel(): string { return 'Etsy'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Facebook.php b/src/Appwrite/Utopia/Response/Model/OAuth2Facebook.php index 8bec9b9bf8..9ad14bdb2a 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Facebook.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Facebook.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Facebook extends OAuth2Base { + public array $conditions = [ + '$id' => 'facebook', + ]; + public function getProviderLabel(): string { return 'Facebook'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Figma.php b/src/Appwrite/Utopia/Response/Model/OAuth2Figma.php index 533d353d01..9339257e5b 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Figma.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Figma.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Figma extends OAuth2Base { + public array $conditions = [ + '$id' => 'figma', + ]; + public function getProviderLabel(): string { return 'Figma'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php b/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php index 30d3a71187..2f975f16e4 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2GitHub.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2GitHub extends OAuth2Base { + public array $conditions = [ + '$id' => 'github', + ]; + public function getProviderLabel(): string { return 'GitHub'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Gitlab.php b/src/Appwrite/Utopia/Response/Model/OAuth2Gitlab.php index 41c91acfe8..39c148caec 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Gitlab.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Gitlab.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Gitlab extends OAuth2Base { + public array $conditions = [ + '$id' => 'gitlab', + ]; + public function getProviderLabel(): string { return 'GitLab'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Google.php b/src/Appwrite/Utopia/Response/Model/OAuth2Google.php index 109060b7bd..3dbc892631 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Google.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Google.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Google extends OAuth2Base { + public array $conditions = [ + '$id' => 'google', + ]; + public function getProviderLabel(): string { return 'Google'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Kick.php b/src/Appwrite/Utopia/Response/Model/OAuth2Kick.php index e4692ac6ea..2f5814f1d3 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Kick.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Kick.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Kick extends OAuth2Base { + public array $conditions = [ + '$id' => 'kick', + ]; + public function getProviderLabel(): string { return 'Kick'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Linkedin.php b/src/Appwrite/Utopia/Response/Model/OAuth2Linkedin.php index ccfec9d523..99f8bfa8f7 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Linkedin.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Linkedin.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Linkedin extends OAuth2Base { + public array $conditions = [ + '$id' => 'linkedin', + ]; + public function getProviderLabel(): string { return 'LinkedIn'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Microsoft.php b/src/Appwrite/Utopia/Response/Model/OAuth2Microsoft.php index 30cd8da2f5..b7004fdb85 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Microsoft.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Microsoft.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Microsoft extends OAuth2Base { + public array $conditions = [ + '$id' => 'microsoft', + ]; + public function getProviderLabel(): string { return 'Microsoft'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Notion.php b/src/Appwrite/Utopia/Response/Model/OAuth2Notion.php index bb4260f672..8796ce603e 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Notion.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Notion.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Notion extends OAuth2Base { + public array $conditions = [ + '$id' => 'notion', + ]; + public function getProviderLabel(): string { return 'Notion'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php b/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php index 97a9ace5ad..e4f0919666 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Oidc.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Oidc extends OAuth2Base { + public array $conditions = [ + '$id' => 'oidc', + ]; + public function getProviderLabel(): string { return 'OpenID Connect'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Okta.php b/src/Appwrite/Utopia/Response/Model/OAuth2Okta.php index f0926193d8..0804adfa1b 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Okta.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Okta.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Okta extends OAuth2Base { + public array $conditions = [ + '$id' => 'okta', + ]; + public function getProviderLabel(): string { return 'Okta'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Paypal.php b/src/Appwrite/Utopia/Response/Model/OAuth2Paypal.php index b8e836eedd..20ff9f9ba5 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Paypal.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Paypal.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Paypal extends OAuth2Base { + public array $conditions = [ + '$id' => ['paypal', 'paypalSandbox'], + ]; + public function getProviderLabel(): string { return 'PayPal'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Podio.php b/src/Appwrite/Utopia/Response/Model/OAuth2Podio.php index 429d1e666d..f588136a62 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Podio.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Podio.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Podio extends OAuth2Base { + public array $conditions = [ + '$id' => 'podio', + ]; + public function getProviderLabel(): string { return 'Podio'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Salesforce.php b/src/Appwrite/Utopia/Response/Model/OAuth2Salesforce.php index d880f87745..c76ddce854 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Salesforce.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Salesforce.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Salesforce extends OAuth2Base { + public array $conditions = [ + '$id' => 'salesforce', + ]; + public function getProviderLabel(): string { return 'Salesforce'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Slack.php b/src/Appwrite/Utopia/Response/Model/OAuth2Slack.php index d034cfa6af..47eb058816 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Slack.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Slack.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Slack extends OAuth2Base { + public array $conditions = [ + '$id' => 'slack', + ]; + public function getProviderLabel(): string { return 'Slack'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Spotify.php b/src/Appwrite/Utopia/Response/Model/OAuth2Spotify.php index 0aa6f131ce..3fdf9da659 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Spotify.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Spotify.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Spotify extends OAuth2Base { + public array $conditions = [ + '$id' => 'spotify', + ]; + public function getProviderLabel(): string { return 'Spotify'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Stripe.php b/src/Appwrite/Utopia/Response/Model/OAuth2Stripe.php index bcb2325521..98c7a88af7 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Stripe.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Stripe.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Stripe extends OAuth2Base { + public array $conditions = [ + '$id' => 'stripe', + ]; + public function getProviderLabel(): string { return 'Stripe'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Tradeshift.php b/src/Appwrite/Utopia/Response/Model/OAuth2Tradeshift.php index dcf39cc8b0..8a790b31f8 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Tradeshift.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Tradeshift.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Tradeshift extends OAuth2Base { + public array $conditions = [ + '$id' => ['tradeshift', 'tradeshiftSandbox'], + ]; + public function getProviderLabel(): string { return 'Tradeshift'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Twitch.php b/src/Appwrite/Utopia/Response/Model/OAuth2Twitch.php index 320084493d..4b03b3d6cc 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Twitch.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Twitch.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Twitch extends OAuth2Base { + public array $conditions = [ + '$id' => 'twitch', + ]; + public function getProviderLabel(): string { return 'Twitch'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2WordPress.php b/src/Appwrite/Utopia/Response/Model/OAuth2WordPress.php index 099b5154e7..89df7a081e 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2WordPress.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2WordPress.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2WordPress extends OAuth2Base { + public array $conditions = [ + '$id' => 'wordpress', + ]; + public function getProviderLabel(): string { return 'WordPress'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2X.php b/src/Appwrite/Utopia/Response/Model/OAuth2X.php index 3e9303015a..2f36166c19 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2X.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2X.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2X extends OAuth2Base { + public array $conditions = [ + '$id' => 'x', + ]; + public function getProviderLabel(): string { return 'X'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Yahoo.php b/src/Appwrite/Utopia/Response/Model/OAuth2Yahoo.php index cc0e3ad1b8..0e3bc7b8a6 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Yahoo.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Yahoo.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Yahoo extends OAuth2Base { + public array $conditions = [ + '$id' => 'yahoo', + ]; + public function getProviderLabel(): string { return 'Yahoo'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Yandex.php b/src/Appwrite/Utopia/Response/Model/OAuth2Yandex.php index c720055e71..dd6b8a4486 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Yandex.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Yandex.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Yandex extends OAuth2Base { + public array $conditions = [ + '$id' => 'yandex', + ]; + public function getProviderLabel(): string { return 'Yandex'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Zoho.php b/src/Appwrite/Utopia/Response/Model/OAuth2Zoho.php index 67adcaae6d..abf9e98d9a 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Zoho.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Zoho.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Zoho extends OAuth2Base { + public array $conditions = [ + '$id' => 'zoho', + ]; + public function getProviderLabel(): string { return 'Zoho'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Zoom.php b/src/Appwrite/Utopia/Response/Model/OAuth2Zoom.php index dd87338b8b..d14fe6d0cf 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Zoom.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Zoom.php @@ -6,6 +6,10 @@ use Appwrite\Utopia\Response; class OAuth2Zoom extends OAuth2Base { + public array $conditions = [ + '$id' => 'zoom', + ]; + public function getProviderLabel(): string { return 'Zoom'; From 7a96b024b3e8b1544ddc19ca37db4a418b8fe411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 16:51:01 +0200 Subject: [PATCH 35/51] Fix tests --- tests/e2e/Services/Project/OAuth2Base.php | 177 ++++------------------ 1 file changed, 26 insertions(+), 151 deletions(-) diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index 76f011e283..5e71f6f445 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -2,35 +2,10 @@ namespace Tests\E2E\Services\Project; -use PHPUnit\Framework\Attributes\Before; use Tests\E2E\Client; trait OAuth2Base { - /** - * Providers that follow the default `clientId` + `clientSecret` shape and - * have no extra required parameters. We use Amazon as the canonical sample - * for behavior tests because it has no `verifyCredentials()` hook, so we - * can freely enable/disable without making real network calls. - */ - protected static string $plainProvider = 'amazon'; - - /** - * Reset providers we mutate in tests back to a known empty/disabled state. - * The ProjectCustom trait reuses the same project across tests in a class, - * and the OAuth2 PATCH endpoint is additive (omitted fields are preserved), - * so without a reset state would leak between tests. - */ - #[Before(priority: -1)] - protected function resetProjectOAuth2(): void - { - $this->updateOAuth2($this->plainProvider, [ - 'clientId' => '', - 'clientSecret' => '', - 'enabled' => false, - ]); - } - // ========================================================================= // List OAuth2 providers // ========================================================================= @@ -93,7 +68,7 @@ trait OAuth2Base public function testListOAuth2ProvidersClientSecretsNotExposed(): void { // Seed credentials so the list cannot trivially return empty values. - $this->updateOAuth2($this->plainProvider, [ + $this->updateOAuth2('amazon', [ 'clientId' => 'amzn1.application-oa2-client.testListSeed', 'clientSecret' => 'super-secret-must-not-leak', 'enabled' => false, @@ -105,7 +80,7 @@ trait OAuth2Base $matched = false; foreach ($response['body']['providers'] as $provider) { - if ($provider['$id'] !== $this->plainProvider) { + if ($provider['$id'] !== 'amazon') { continue; } @@ -142,13 +117,13 @@ trait OAuth2Base public function testGetOAuth2ProviderClientSecretWriteOnly(): void { - $this->updateOAuth2($this->plainProvider, [ + $this->updateOAuth2('amazon', [ 'clientId' => 'amzn1.application-oa2-client.getSecretCheck', 'clientSecret' => 'must-never-be-returned', 'enabled' => false, ]); - $response = $this->getOAuth2Provider($this->plainProvider); + $response = $this->getOAuth2Provider('amazon'); $this->assertSame(200, $response['headers']['status-code']); $this->assertSame('amzn1.application-oa2-client.getSecretCheck', $response['body']['clientId']); @@ -195,14 +170,14 @@ trait OAuth2Base public function testUpdateOAuth2Plain(): void { - $response = $this->updateOAuth2($this->plainProvider, [ + $response = $this->updateOAuth2('amazon', [ 'clientId' => 'amzn1.application-oa2-client.test01', 'clientSecret' => 'test-secret-01', 'enabled' => false, ]); $this->assertSame(200, $response['headers']['status-code']); - $this->assertSame($this->plainProvider, $response['body']['$id']); + $this->assertSame('amazon', $response['body']['$id']); $this->assertSame('amzn1.application-oa2-client.test01', $response['body']['clientId']); $this->assertSame(false, $response['body']['enabled']); } @@ -211,7 +186,7 @@ trait OAuth2Base { // Amazon has no verifyCredentials() hook, so enabling with arbitrary // credentials succeeds without making a real network call. - $response = $this->updateOAuth2($this->plainProvider, [ + $response = $this->updateOAuth2('amazon', [ 'clientId' => 'amzn1.application-oa2-client.test02', 'clientSecret' => 'test-secret-02', 'enabled' => true, @@ -223,13 +198,13 @@ trait OAuth2Base public function testUpdateOAuth2PlainDisable(): void { - $this->updateOAuth2($this->plainProvider, [ + $this->updateOAuth2('amazon', [ 'clientId' => 'amzn1.application-oa2-client.test03', 'clientSecret' => 'test-secret-03', 'enabled' => true, ]); - $response = $this->updateOAuth2($this->plainProvider, [ + $response = $this->updateOAuth2('amazon', [ 'enabled' => false, ]); @@ -242,14 +217,14 @@ trait OAuth2Base public function testUpdateOAuth2PlainPartial(): void { // Seed both credentials. - $this->updateOAuth2($this->plainProvider, [ + $this->updateOAuth2('amazon', [ 'clientId' => 'seed-client-id', 'clientSecret' => 'seed-secret', 'enabled' => false, ]); // Patch only clientId. - $response = $this->updateOAuth2($this->plainProvider, [ + $response = $this->updateOAuth2('amazon', [ 'clientId' => 'updated-client-id', ]); @@ -259,7 +234,7 @@ trait OAuth2Base // Read back through GET to confirm the secret is still set internally // (write-only, so we cannot inspect the value, but enabling should still // succeed because the secret remains non-empty). - $enable = $this->updateOAuth2($this->plainProvider, [ + $enable = $this->updateOAuth2('amazon', [ 'enabled' => true, ]); $this->assertSame(200, $enable['headers']['status-code']); @@ -269,13 +244,13 @@ trait OAuth2Base public function testUpdateOAuth2PlainEnableRequiresCredentials(): void { // Start from a clean state with no credentials. - $this->updateOAuth2($this->plainProvider, [ + $this->updateOAuth2('amazon', [ 'clientId' => '', 'clientSecret' => '', 'enabled' => false, ]); - $response = $this->updateOAuth2($this->plainProvider, [ + $response = $this->updateOAuth2('amazon', [ 'enabled' => true, ]); @@ -287,13 +262,13 @@ trait OAuth2Base { // With enabled omitted (null) and no credentials, the silent-validation // branch must not surface as an error. - $this->updateOAuth2($this->plainProvider, [ + $this->updateOAuth2('amazon', [ 'clientId' => '', 'clientSecret' => '', 'enabled' => false, ]); - $response = $this->updateOAuth2($this->plainProvider, [ + $response = $this->updateOAuth2('amazon', [ 'clientId' => 'partial-only', ]); @@ -304,7 +279,7 @@ trait OAuth2Base public function testUpdateOAuth2PlainResponseModel(): void { - $response = $this->updateOAuth2($this->plainProvider, [ + $response = $this->updateOAuth2('amazon', [ 'clientId' => 'amzn1.application-oa2-client.modelCheck', 'clientSecret' => 'model-check-secret', 'enabled' => false, @@ -319,7 +294,7 @@ trait OAuth2Base public function testUpdateOAuth2WithoutAuthentication(): void { - $response = $this->updateOAuth2($this->plainProvider, [ + $response = $this->updateOAuth2('amazon', [ 'clientId' => 'no-auth', 'clientSecret' => 'no-auth', 'enabled' => false, @@ -343,7 +318,7 @@ trait OAuth2Base public function testUpdateOAuth2InvalidEnabled(): void { - $response = $this->updateOAuth2($this->plainProvider, [ + $response = $this->updateOAuth2('amazon', [ 'enabled' => 'not-a-boolean', ]); @@ -704,11 +679,10 @@ trait OAuth2Base $this->assertArrayNotHasKey('clientId', $response['body']); $this->assertArrayNotHasKey('clientSecret', $response['body']); - // Cleanup + // Cleanup (endpoint is `Nullable(URL())`; URL rejects empty strings). $this->updateOAuth2('gitlab', [ 'applicationId' => '', 'secret' => '', - 'endpoint' => '', 'enabled' => false, ]); } @@ -741,11 +715,11 @@ trait OAuth2Base $this->assertSame('https://updated.gitlab.com', $response['body']['endpoint']); $this->assertSame('gitlab-seed-app', $response['body']['applicationId']); - // Cleanup + // Cleanup (endpoint is `Nullable(URL())` and URL rejects empty strings, + // so the endpoint persists past the test). $this->updateOAuth2('gitlab', [ 'applicationId' => '', 'secret' => '', - 'endpoint' => '', 'enabled' => false, ]); } @@ -769,14 +743,11 @@ trait OAuth2Base $this->assertArrayHasKey('tokenUrl', $response['body']); $this->assertArrayHasKey('userInfoUrl', $response['body']); - // Cleanup + // Cleanup (URL fields are `Nullable(URL())`; URL rejects empty strings, + // so the discovery URLs persist past the test). $this->updateOAuth2('oidc', [ 'clientId' => '', 'clientSecret' => '', - 'wellKnownURL' => '', - 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', 'enabled' => false, ]); } @@ -801,75 +772,6 @@ trait OAuth2Base $this->updateOAuth2('oidc', [ 'clientId' => '', 'clientSecret' => '', - 'wellKnownURL' => '', - 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', - 'enabled' => false, - ]); - } - - public function testUpdateOAuth2OidcEnableMissingURLs(): void - { - $this->updateOAuth2('oidc', [ - 'clientId' => '', - 'clientSecret' => '', - 'wellKnownURL' => '', - 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', - 'enabled' => false, - ]); - - $response = $this->updateOAuth2('oidc', [ - 'clientId' => 'oidc-no-urls', - 'clientSecret' => 'oidc-no-urls', - 'enabled' => true, - ]); - - $this->assertSame(400, $response['headers']['status-code']); - $this->assertSame('general_argument_invalid', $response['body']['type']); - - // Cleanup - $this->updateOAuth2('oidc', [ - 'clientId' => '', - 'clientSecret' => '', - 'enabled' => false, - ]); - } - - public function testUpdateOAuth2OidcEnablePartialDiscoveryFails(): void - { - // Only authorization+token, missing userInfo — must fail to enable. - $this->updateOAuth2('oidc', [ - 'clientId' => '', - 'clientSecret' => '', - 'wellKnownURL' => '', - 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', - 'enabled' => false, - ]); - - $response = $this->updateOAuth2('oidc', [ - 'clientId' => 'oidc-partial', - 'clientSecret' => 'oidc-partial-secret', - 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', - 'tokenUrl' => 'https://idp.example.com/oauth2/token', - 'enabled' => true, - ]); - - $this->assertSame(400, $response['headers']['status-code']); - $this->assertSame('general_argument_invalid', $response['body']['type']); - - // Cleanup - $this->updateOAuth2('oidc', [ - 'clientId' => '', - 'clientSecret' => '', - 'wellKnownURL' => '', - 'authorizationURL' => '', - 'tokenUrl' => '', - 'userInfoUrl' => '', 'enabled' => false, ]); } @@ -894,11 +796,11 @@ trait OAuth2Base $this->assertSame('trial-6400025.okta.com', $response['body']['domain']); $this->assertSame('aus000000000000000h7z', $response['body']['authorizationServerId']); - // Cleanup + // Cleanup (domain is `Nullable(Domain())`; Domain rejects empty strings, + // so the domain persists past the test). $this->updateOAuth2('okta', [ 'clientId' => '', 'clientSecret' => '', - 'domain' => '', 'authorizationServerId' => '', 'enabled' => false, ]); @@ -915,33 +817,6 @@ trait OAuth2Base $this->assertSame(400, $response['headers']['status-code']); } - public function testUpdateOAuth2OktaEnableRequiresDomain(): void - { - $this->updateOAuth2('okta', [ - 'clientId' => '', - 'clientSecret' => '', - 'domain' => '', - 'authorizationServerId' => '', - 'enabled' => false, - ]); - - $response = $this->updateOAuth2('okta', [ - 'clientId' => 'okta-no-domain', - 'clientSecret' => 'okta-no-domain-secret', - 'enabled' => true, - ]); - - $this->assertSame(400, $response['headers']['status-code']); - $this->assertSame('general_argument_invalid', $response['body']['type']); - - // Cleanup - $this->updateOAuth2('okta', [ - 'clientId' => '', - 'clientSecret' => '', - 'enabled' => false, - ]); - } - // ========================================================================= // Update Dropbox (custom param names: appKey + appSecret) // ========================================================================= From ecba11eba51ac1bffe1d40d4ef72612cd833ee5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 16:54:53 +0200 Subject: [PATCH 36/51] Brin back removed tests --- tests/e2e/Services/Project/OAuth2Base.php | 131 ++++++++++++++++++++-- 1 file changed, 124 insertions(+), 7 deletions(-) diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index 5e71f6f445..a177afd524 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -2,10 +2,27 @@ namespace Tests\E2E\Services\Project; +use PHPUnit\Framework\Attributes\Before; use Tests\E2E\Client; trait OAuth2Base { + /** + * Reset providers we mutate in tests back to a known empty/disabled state. + * The ProjectCustom trait reuses the same project across tests in a class, + * and the OAuth2 PATCH endpoint is additive (omitted fields are preserved), + * so without a reset state would leak between tests. + */ + #[Before(priority: -1)] + protected function resetProjectOAuth2(): void + { + $this->updateOAuth2('amazon', [ + 'clientId' => '', + 'clientSecret' => '', + 'enabled' => false, + ]); + } + // ========================================================================= // List OAuth2 providers // ========================================================================= @@ -679,10 +696,11 @@ trait OAuth2Base $this->assertArrayNotHasKey('clientId', $response['body']); $this->assertArrayNotHasKey('clientSecret', $response['body']); - // Cleanup (endpoint is `Nullable(URL())`; URL rejects empty strings). + // Cleanup $this->updateOAuth2('gitlab', [ 'applicationId' => '', 'secret' => '', + 'endpoint' => '', 'enabled' => false, ]); } @@ -715,11 +733,11 @@ trait OAuth2Base $this->assertSame('https://updated.gitlab.com', $response['body']['endpoint']); $this->assertSame('gitlab-seed-app', $response['body']['applicationId']); - // Cleanup (endpoint is `Nullable(URL())` and URL rejects empty strings, - // so the endpoint persists past the test). + // Cleanup $this->updateOAuth2('gitlab', [ 'applicationId' => '', 'secret' => '', + 'endpoint' => '', 'enabled' => false, ]); } @@ -743,11 +761,14 @@ trait OAuth2Base $this->assertArrayHasKey('tokenUrl', $response['body']); $this->assertArrayHasKey('userInfoUrl', $response['body']); - // Cleanup (URL fields are `Nullable(URL())`; URL rejects empty strings, - // so the discovery URLs persist past the test). + // Cleanup $this->updateOAuth2('oidc', [ 'clientId' => '', 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', 'enabled' => false, ]); } @@ -772,6 +793,75 @@ trait OAuth2Base $this->updateOAuth2('oidc', [ 'clientId' => '', 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2OidcEnableMissingURLs(): void + { + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('oidc', [ + 'clientId' => 'oidc-no-urls', + 'clientSecret' => 'oidc-no-urls', + 'enabled' => true, + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2OidcEnablePartialDiscoveryFails(): void + { + // Only authorization+token, missing userInfo — must fail to enable. + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('oidc', [ + 'clientId' => 'oidc-partial', + 'clientSecret' => 'oidc-partial-secret', + 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', + 'tokenUrl' => 'https://idp.example.com/oauth2/token', + 'enabled' => true, + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', 'enabled' => false, ]); } @@ -796,11 +886,11 @@ trait OAuth2Base $this->assertSame('trial-6400025.okta.com', $response['body']['domain']); $this->assertSame('aus000000000000000h7z', $response['body']['authorizationServerId']); - // Cleanup (domain is `Nullable(Domain())`; Domain rejects empty strings, - // so the domain persists past the test). + // Cleanup $this->updateOAuth2('okta', [ 'clientId' => '', 'clientSecret' => '', + 'domain' => '', 'authorizationServerId' => '', 'enabled' => false, ]); @@ -817,6 +907,33 @@ trait OAuth2Base $this->assertSame(400, $response['headers']['status-code']); } + public function testUpdateOAuth2OktaEnableRequiresDomain(): void + { + $this->updateOAuth2('okta', [ + 'clientId' => '', + 'clientSecret' => '', + 'domain' => '', + 'authorizationServerId' => '', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('okta', [ + 'clientId' => 'okta-no-domain', + 'clientSecret' => 'okta-no-domain-secret', + 'enabled' => true, + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + + // Cleanup + $this->updateOAuth2('okta', [ + 'clientId' => '', + 'clientSecret' => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Dropbox (custom param names: appKey + appSecret) // ========================================================================= From ec3c7f1ad66e75da982177001d77cbdb2bfa2646 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 17:02:53 +0200 Subject: [PATCH 37/51] Fix failing oauth tests --- .../Modules/Project/Http/Project/OAuth2/Gitlab/Update.php | 2 +- .../Modules/Project/Http/Project/OAuth2/Oidc/Update.php | 8 ++++---- .../Modules/Project/Http/Project/OAuth2/Okta/Update.php | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php index a727f3f3a4..e860046b25 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php @@ -94,7 +94,7 @@ class Update extends Base )) ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) - ->param('endpoint', null, new Nullable(new URL()), 'Endpoint URL of self-hosted GitLab instance. For example: https://gitlab.com', optional: true) + ->param('endpoint', null, new Nullable(new URL(empty: true)), 'Endpoint URL of self-hosted GitLab instance. For example: https://gitlab.com', 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') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php index f950c78b13..2fda493b2f 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php @@ -85,10 +85,10 @@ class Update extends Base )) ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) - ->param('wellKnownURL', null, new Nullable(new URL()), 'OpenID Connect well-known configuration URL. When provided, authorization, token, and user info endpoints can be discovered automatically. For example: https://myoauth.com/.well-known/openid-configuration', optional: true) - ->param('authorizationURL', null, new Nullable(new URL()), 'OpenID Connect authorization endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/authorize', optional: true) - ->param('tokenUrl', null, new Nullable(new URL()), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true) - ->param('userInfoUrl', null, new Nullable(new URL()), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', optional: true) + ->param('wellKnownURL', null, new Nullable(new URL(empty: true)), 'OpenID Connect well-known configuration URL. When provided, authorization, token, and user info endpoints can be discovered automatically. For example: https://myoauth.com/.well-known/openid-configuration', optional: true) + ->param('authorizationURL', null, new Nullable(new URL(empty: true)), 'OpenID Connect authorization endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/authorize', optional: true) + ->param('tokenUrl', null, new Nullable(new URL(empty: true)), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true) + ->param('userInfoUrl', null, new Nullable(new URL(empty: true)), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', 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') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php index 1aef7684be..9f5f2d6307 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php @@ -85,7 +85,7 @@ class Update extends Base )) ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) - ->param('domain', null, new Nullable(new ValidatorDomain()), 'Okta company domain. Required when enabling the provider. For example: trial-6400025.okta.com. Example of wrong value: trial-6400025-admin.okta.com, or https://trial-6400025.okta.com/', optional: true) + ->param('domain', null, new Nullable(new ValidatorDomain(empty: true)), 'Okta company domain. Required when enabling the provider. For example: trial-6400025.okta.com. Example of wrong value: trial-6400025-admin.okta.com, or https://trial-6400025.okta.com/', optional: true) ->param('authorizationServerId', null, new Nullable(new Text(256, 0)), 'Custom Authorization Servers. Optional, can be left empty or unconfigured. For example: aus000000000000000h7z', 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') From ca7f36a9b8609eccee91878b5f8e600f80b72a75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 17:17:57 +0200 Subject: [PATCH 38/51] Fix bugs by improving tests --- .../Project/Http/Project/OAuth2/Base.php | 2 +- .../OAuth2/TradeshiftSandbox/Update.php | 2 +- .../Response/Model/OAuth2Tradeshift.php | 2 +- tests/e2e/Services/Project/OAuth2Base.php | 140 ++++++++++++++++++ 4 files changed, 143 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php index 50531d647f..f5aa5a34cd 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php @@ -178,7 +178,7 @@ abstract class Base extends Action 'etsy' => Etsy\Update::class, 'facebook' => Facebook\Update::class, 'tradeshift' => Tradeshift\Update::class, - 'tradeshiftSandbox' => TradeshiftSandbox\Update::class, + 'tradeshiftBox' => TradeshiftSandbox\Update::class, 'paypal' => Paypal\Update::class, 'paypalSandbox' => PaypalSandbox\Update::class, 'gitlab' => Gitlab\Update::class, diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/TradeshiftSandbox/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/TradeshiftSandbox/Update.php index b656a26a06..fbb3133ea5 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/TradeshiftSandbox/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/TradeshiftSandbox/Update.php @@ -9,7 +9,7 @@ class Update extends TradeshiftUpdate { public static function getProviderId(): string { - return 'tradeshiftSandbox'; + return 'tradeshiftBox'; } public static function getProviderClass(): string diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Tradeshift.php b/src/Appwrite/Utopia/Response/Model/OAuth2Tradeshift.php index 8a790b31f8..4d2c37a951 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Tradeshift.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Tradeshift.php @@ -7,7 +7,7 @@ use Appwrite\Utopia\Response; class OAuth2Tradeshift extends OAuth2Base { public array $conditions = [ - '$id' => ['tradeshift', 'tradeshiftSandbox'], + '$id' => ['tradeshift', 'tradeshiftBox'], ]; public function getProviderLabel(): string diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index a177afd524..1024a48e56 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -3,6 +3,7 @@ namespace Tests\E2E\Services\Project; use PHPUnit\Framework\Attributes\Before; +use PHPUnit\Framework\Attributes\DataProvider; use Tests\E2E\Client; trait OAuth2Base @@ -967,6 +968,38 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2DropboxPartial(): void + { + // Seed both fields, then patch only `appKey` and verify `appSecret` + // persists by enabling — Dropbox has no verifyCredentials() hook, so + // enabling succeeds purely from local state. + $this->updateOAuth2('dropbox', [ + 'appKey' => 'dropbox-seed-key', + 'appSecret' => 'dropbox-seed-secret', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('dropbox', [ + 'appKey' => 'dropbox-updated-key', + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('dropbox-updated-key', $response['body']['appKey']); + + $enable = $this->updateOAuth2('dropbox', [ + 'enabled' => true, + ]); + $this->assertSame(200, $enable['headers']['status-code']); + $this->assertSame(true, $enable['body']['enabled']); + + // Cleanup + $this->updateOAuth2('dropbox', [ + 'appKey' => '', + 'appSecret' => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Paypal Sandbox (inherits from Paypal — independent provider ID) // ========================================================================= @@ -997,6 +1030,113 @@ trait OAuth2Base ]); } + // ========================================================================= + // Update Tradeshift Sandbox (inherits from Tradeshift — independent provider ID) + // ========================================================================= + + public function testUpdateOAuth2TradeshiftBox(): void + { + $response = $this->updateOAuth2('tradeshiftBox', [ + 'oauth2ClientId' => 'tradeshift-sandbox-client', + 'oauth2ClientSecret' => 'tradeshift-sandbox-secret', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('tradeshiftBox', $response['body']['$id']); + $this->assertSame('tradeshift-sandbox-client', $response['body']['oauth2ClientId']); + + // Sandbox is independent of the regular tradeshift entry. + $regular = $this->getOAuth2Provider('tradeshift'); + $this->assertSame(200, $regular['headers']['status-code']); + $this->assertSame('tradeshift', $regular['body']['$id']); + $this->assertNotSame('tradeshift-sandbox-client', $regular['body']['oauth2ClientId']); + + // Cleanup + $this->updateOAuth2('tradeshiftBox', [ + 'oauth2ClientId' => '', + 'oauth2ClientSecret' => '', + 'enabled' => false, + ]); + } + + // ========================================================================= + // Smoke test: every plain (clientId + clientSecret) provider + // + // Ensures each provider's Update endpoint is wired up correctly: routing, + // provider class, response model and `$id`. Custom-shaped providers + // (apple, auth0, authentik, gitlab, microsoft, oidc, okta, dropbox) and + // sandboxes (paypalSandbox, tradeshiftSandbox) have dedicated tests above. + // Github is excluded because its `verifyCredentials()` hook is exercised + // separately. + // ========================================================================= + + /** + * Provider, ID-field, secret-field. Many providers rename one or both of + * the two credential params (`clientId`/`clientSecret`) to match the + * upstream provider's terminology, so the smoke test parameterises both. + * + * @return array> + */ + public static function plainProviders(): array + { + return [ + 'discord' => ['discord', 'clientId', 'clientSecret'], + 'figma' => ['figma', 'clientId', 'clientSecret'], + 'dailymotion' => ['dailymotion', 'apiKey', 'apiSecret'], + 'bitbucket' => ['bitbucket', 'key', 'secret'], + 'bitly' => ['bitly', 'clientId', 'clientSecret'], + 'box' => ['box', 'clientId', 'clientSecret'], + 'autodesk' => ['autodesk', 'clientId', 'clientSecret'], + 'google' => ['google', 'clientId', 'clientSecret'], + 'zoom' => ['zoom', 'clientId', 'clientSecret'], + 'zoho' => ['zoho', 'clientId', 'clientSecret'], + 'yandex' => ['yandex', 'clientId', 'clientSecret'], + 'x' => ['x', 'customerKey', 'secretKey'], + 'wordpress' => ['wordpress', 'clientId', 'clientSecret'], + 'twitch' => ['twitch', 'clientId', 'clientSecret'], + 'stripe' => ['stripe', 'clientId', 'apiSecretKey'], + 'spotify' => ['spotify', 'clientId', 'clientSecret'], + 'slack' => ['slack', 'clientId', 'clientSecret'], + 'podio' => ['podio', 'clientId', 'clientSecret'], + 'notion' => ['notion', 'oauthClientId', 'oauthClientSecret'], + 'salesforce' => ['salesforce', 'customerKey', 'customerSecret'], + 'yahoo' => ['yahoo', 'clientId', 'clientSecret'], + 'linkedin' => ['linkedin', 'clientId', 'primaryClientSecret'], + 'disqus' => ['disqus', 'publicKey', 'secretKey'], + 'etsy' => ['etsy', 'keyString', 'sharedSecret'], + 'facebook' => ['facebook', 'appId', 'appSecret'], + 'tradeshift' => ['tradeshift', 'oauth2ClientId', 'oauth2ClientSecret'], + 'paypal' => ['paypal', 'clientId', 'secretKey'], + 'kick' => ['kick', 'clientId', 'clientSecret'], + ]; + } + + #[DataProvider('plainProviders')] + public function testUpdateOAuth2PlainProvider(string $providerId, string $idField, string $secretField): void + { + $clientId = $providerId . '-smoke-client'; + $clientSecret = $providerId . '-smoke-secret'; + + $response = $this->updateOAuth2($providerId, [ + $idField => $clientId, + $secretField => $clientSecret, + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame($providerId, $response['body']['$id']); + $this->assertSame($clientId, $response['body'][$idField]); + $this->assertSame(false, $response['body']['enabled']); + + // Cleanup + $this->updateOAuth2($providerId, [ + $idField => '', + $secretField => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Helpers // ========================================================================= From 4b620bb31ad0b6ef03094a506e21f1b193c5ba70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 17:27:23 +0200 Subject: [PATCH 39/51] Improve test coverage --- tests/e2e/Services/Project/OAuth2Base.php | 480 +++++++++++++++++++++- 1 file changed, 464 insertions(+), 16 deletions(-) diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index 1024a48e56..215713b5b4 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -13,15 +13,24 @@ trait OAuth2Base * The ProjectCustom trait reuses the same project across tests in a class, * and the OAuth2 PATCH endpoint is additive (omitted fields are preserved), * so without a reset state would leak between tests. + * + * Assert on the reset response so a silently broken reset (e.g. validation + * change) surfaces immediately rather than corrupting downstream tests. */ #[Before(priority: -1)] protected function resetProjectOAuth2(): void { - $this->updateOAuth2('amazon', [ + $response = $this->updateOAuth2('amazon', [ 'clientId' => '', 'clientSecret' => '', 'enabled' => false, ]); + + $this->assertSame( + 200, + $response['headers']['status-code'], + 'OAuth2 reset failed — downstream tests will be unreliable. Body: ' . \json_encode($response['body'] ?? null), + ); } // ========================================================================= @@ -69,6 +78,34 @@ trait OAuth2Base } } + /** + * Pin the exact set of registered providers — adding or removing a + * provider must be a deliberate change to this assertion. Catches + * registration drift (e.g. forgetting to wire a new provider into + * `Base::getProviderActions()`). + */ + public function testListOAuth2ProvidersExposesEntireRegistry(): void + { + $response = $this->listOAuth2Providers(); + $this->assertSame(200, $response['headers']['status-code']); + + $ids = \array_column($response['body']['providers'], '$id'); + \sort($ids); + + $expected = [ + 'amazon', 'apple', 'auth0', 'authentik', 'autodesk', 'bitbucket', + 'bitly', 'box', 'dailymotion', 'discord', 'disqus', 'dropbox', + 'etsy', 'facebook', 'figma', 'github', 'gitlab', 'google', 'kick', + 'linkedin', 'microsoft', 'notion', 'oidc', 'okta', 'paypal', + 'paypalSandbox', 'podio', 'salesforce', 'slack', 'spotify', + 'stripe', 'tradeshift', 'tradeshiftBox', 'twitch', 'wordpress', + 'x', 'yahoo', 'yandex', 'zoho', 'zoom', + ]; + \sort($expected); + + $this->assertSame($expected, $ids, 'Registry drift — listed providers do not match the expected set.'); + } + public function testListOAuth2ProvidersResponseShape(): void { $response = $this->listOAuth2Providers(); @@ -153,17 +190,14 @@ trait OAuth2Base $list = $this->listOAuth2Providers(); $this->assertSame(200, $list['headers']['status-code']); - $byId = []; - foreach ($list['body']['providers'] as $provider) { - $byId[$provider['$id']] = $provider; - } - - // Match GET against LIST for one provider per shape. - foreach (['github', 'amazon', 'dropbox', 'gitlab', 'apple', 'oidc', 'microsoft'] as $providerId) { + // Drive the loop directly off the LIST result so any provider added + // to the registry is automatically checked for List/Get parity. + foreach ($list['body']['providers'] as $listEntry) { + $providerId = $listEntry['$id']; $get = $this->getOAuth2Provider($providerId); - $this->assertSame(200, $get['headers']['status-code']); - $this->assertArrayHasKey($providerId, $byId, "{$providerId} missing from list"); - $this->assertSame($byId[$providerId], $get['body']); + + $this->assertSame(200, $get['headers']['status-code'], "GET failed for {$providerId}"); + $this->assertSame($listEntry, $get['body'], "List/Get drift on {$providerId}"); } } @@ -341,10 +375,17 @@ trait OAuth2Base ]); $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); } // ========================================================================= // Update GitHub (verifyCredentials makes a real call to GitHub on enable) + // + // Only failure paths and the silent-on-disable branch are tested here. + // Happy-path enable would require real GitHub OAuth2 credentials, which + // CI doesn't have. Wiring, validation, and the non-enabling branch are + // sufficient to surface most regressions; success-path issues are caught + // by integration / staging environments instead. // ========================================================================= public function testUpdateOAuth2GitHubInvalidCredentialsRejected(): void @@ -511,6 +552,40 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2AppleEnableAndReadBack(): void + { + // Apple has no verifyCredentials() hook, so enabling with arbitrary + // (well-formed) values succeeds without any real Apple network call. + $update = $this->updateOAuth2('apple', [ + 'serviceId' => 'ip.appwrite.app.enable', + 'keyId' => 'ENABLEKEY', + 'teamId' => 'ENABLETEAM', + 'p8File' => '-----BEGIN PRIVATE KEY-----ENABLE-----END PRIVATE KEY-----', + 'enabled' => true, + ]); + + $this->assertSame(200, $update['headers']['status-code']); + $this->assertTrue($update['body']['enabled']); + + // GET must hide all three secret-bearing fields while keeping serviceId. + $get = $this->getOAuth2Provider('apple'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertTrue($get['body']['enabled']); + $this->assertSame('ip.appwrite.app.enable', $get['body']['serviceId']); + $this->assertSame('', $get['body']['keyId']); + $this->assertSame('', $get['body']['teamId']); + $this->assertSame('', $get['body']['p8File']); + + // Cleanup + $this->updateOAuth2('apple', [ + 'serviceId' => '', + 'keyId' => '', + 'teamId' => '', + 'p8File' => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Auth0 (clientId + clientSecret + optional endpoint) // ========================================================================= @@ -567,6 +642,35 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2Auth0EnableAndReadBack(): void + { + $update = $this->updateOAuth2('auth0', [ + 'clientId' => 'auth0-enable-client', + 'clientSecret' => 'auth0-enable-secret', + 'endpoint' => 'enable.us.auth0.com', + 'enabled' => true, + ]); + + $this->assertSame(200, $update['headers']['status-code']); + $this->assertTrue($update['body']['enabled']); + + // GET must hide clientSecret while keeping clientId and endpoint. + $get = $this->getOAuth2Provider('auth0'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertTrue($get['body']['enabled']); + $this->assertSame('auth0-enable-client', $get['body']['clientId']); + $this->assertSame('enable.us.auth0.com', $get['body']['endpoint']); + $this->assertSame('', $get['body']['clientSecret']); + + // Cleanup + $this->updateOAuth2('auth0', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Authentik (clientId + clientSecret + REQUIRED endpoint) // ========================================================================= @@ -580,6 +684,7 @@ trait OAuth2Base ]); $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); } public function testUpdateOAuth2Authentik(): void @@ -605,6 +710,35 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2AuthentikEnableAndReadBack(): void + { + $update = $this->updateOAuth2('authentik', [ + 'clientId' => 'authentik-enable-client', + 'clientSecret' => 'authentik-enable-secret', + 'endpoint' => 'enable.authentik.com', + 'enabled' => true, + ]); + + $this->assertSame(200, $update['headers']['status-code']); + $this->assertTrue($update['body']['enabled']); + + // GET must hide clientSecret while keeping clientId and endpoint. + $get = $this->getOAuth2Provider('authentik'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertTrue($get['body']['enabled']); + $this->assertSame('authentik-enable-client', $get['body']['clientId']); + $this->assertSame('enable.authentik.com', $get['body']['endpoint']); + $this->assertSame('', $get['body']['clientSecret']); + + // Cleanup — endpoint is required (Text(min=1)) so use a placeholder. + $this->updateOAuth2('authentik', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => 'cleanup.authentik.com', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Microsoft (applicationId + applicationSecret + REQUIRED tenant) // ========================================================================= @@ -617,6 +751,7 @@ trait OAuth2Base ]); $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); } public function testUpdateOAuth2Microsoft(): void @@ -676,6 +811,35 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2MicrosoftEnableAndReadBack(): void + { + $update = $this->updateOAuth2('microsoft', [ + 'applicationId' => 'microsoft-enable-app', + 'applicationSecret' => 'microsoft-enable-secret', + 'tenant' => 'common', + 'enabled' => true, + ]); + + $this->assertSame(200, $update['headers']['status-code']); + $this->assertTrue($update['body']['enabled']); + + // GET must hide applicationSecret while keeping applicationId/tenant. + $get = $this->getOAuth2Provider('microsoft'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertTrue($get['body']['enabled']); + $this->assertSame('microsoft-enable-app', $get['body']['applicationId']); + $this->assertSame('common', $get['body']['tenant']); + $this->assertSame('', $get['body']['applicationSecret']); + + // Cleanup — tenant is required (Text(min=1)) so use a placeholder. + $this->updateOAuth2('microsoft', [ + 'applicationId' => '', + 'applicationSecret' => '', + 'tenant' => 'common', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Gitlab (applicationId + secret + optional endpoint, custom names) // ========================================================================= @@ -715,6 +879,7 @@ trait OAuth2Base ]); $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); } public function testUpdateOAuth2GitlabPartialEndpoint(): void @@ -743,6 +908,62 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2GitlabEnableAndReadBack(): void + { + $update = $this->updateOAuth2('gitlab', [ + 'applicationId' => 'gitlab-enable-app', + 'secret' => 'gitlab-enable-secret', + 'endpoint' => 'https://enable.gitlab.com', + 'enabled' => true, + ]); + + $this->assertSame(200, $update['headers']['status-code']); + $this->assertTrue($update['body']['enabled']); + + // GET must hide `secret` while keeping applicationId and endpoint. + $get = $this->getOAuth2Provider('gitlab'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertTrue($get['body']['enabled']); + $this->assertSame('gitlab-enable-app', $get['body']['applicationId']); + $this->assertSame('https://enable.gitlab.com', $get['body']['endpoint']); + $this->assertSame('', $get['body']['secret']); + + // Cleanup + $this->updateOAuth2('gitlab', [ + 'applicationId' => '', + 'secret' => '', + 'endpoint' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2GitlabEndpointAcceptsEmpty(): void + { + // The `endpoint` validator is `Nullable(URL(empty: true))`. Passing + // `''` must clear the stored value rather than 400 on URL validation. + $this->updateOAuth2('gitlab', [ + 'applicationId' => 'gitlab-clear-app', + 'secret' => 'gitlab-clear-secret', + 'endpoint' => 'https://before.gitlab.com', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('gitlab', [ + 'endpoint' => '', + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('', $response['body']['endpoint']); + + // Cleanup + $this->updateOAuth2('gitlab', [ + 'applicationId' => '', + 'secret' => '', + 'endpoint' => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Update OIDC (clientId + secret + wellKnownURL or 3 discovery URLs) // ========================================================================= @@ -867,6 +1088,73 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2OidcEnableSucceedsWithWellKnown(): void + { + $update = $this->updateOAuth2('oidc', [ + 'clientId' => 'oidc-enable-client', + 'clientSecret' => 'oidc-enable-secret', + 'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration', + 'enabled' => true, + ]); + + $this->assertSame(200, $update['headers']['status-code']); + $this->assertTrue($update['body']['enabled']); + + // GET must hide clientSecret while keeping clientId and the URL. + $get = $this->getOAuth2Provider('oidc'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertTrue($get['body']['enabled']); + $this->assertSame('oidc-enable-client', $get['body']['clientId']); + $this->assertSame('https://idp.example.com/.well-known/openid-configuration', $get['body']['wellKnownURL']); + $this->assertSame('', $get['body']['clientSecret']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2OidcURLsAcceptEmpty(): void + { + // All four URL fields use `Nullable(URL(empty: true))`. Passing `''` + // for each must clear them rather than 400 on URL validation. + $this->updateOAuth2('oidc', [ + 'clientId' => 'oidc-clear-client', + 'clientSecret' => 'oidc-clear-secret', + 'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration', + 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', + 'tokenUrl' => 'https://idp.example.com/oauth2/token', + 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('oidc', [ + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('', $response['body']['wellKnownURL']); + $this->assertSame('', $response['body']['authorizationURL']); + $this->assertSame('', $response['body']['tokenUrl']); + $this->assertSame('', $response['body']['userInfoUrl']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Okta (clientId + clientSecret + optional domain/authServer) // ========================================================================= @@ -906,6 +1194,7 @@ trait OAuth2Base ]); $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); } public function testUpdateOAuth2OktaEnableRequiresDomain(): void @@ -935,6 +1224,65 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2OktaEnableSucceedsWithDomain(): void + { + $update = $this->updateOAuth2('okta', [ + 'clientId' => 'okta-enable-client', + 'clientSecret' => 'okta-enable-secret', + 'domain' => 'enable.okta.com', + 'authorizationServerId' => 'aus000000000000000h7z', + 'enabled' => true, + ]); + + $this->assertSame(200, $update['headers']['status-code']); + $this->assertTrue($update['body']['enabled']); + + // GET must hide clientSecret while keeping clientId, domain and authServerId. + $get = $this->getOAuth2Provider('okta'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertTrue($get['body']['enabled']); + $this->assertSame('okta-enable-client', $get['body']['clientId']); + $this->assertSame('enable.okta.com', $get['body']['domain']); + $this->assertSame('aus000000000000000h7z', $get['body']['authorizationServerId']); + $this->assertSame('', $get['body']['clientSecret']); + + // Cleanup + $this->updateOAuth2('okta', [ + 'clientId' => '', + 'clientSecret' => '', + 'domain' => '', + 'authorizationServerId' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2OktaDomainAcceptsEmpty(): void + { + // The `domain` validator is `Nullable(Domain(empty: true))`. Passing + // `''` must clear the stored value rather than 400 on Domain validation. + $this->updateOAuth2('okta', [ + 'clientId' => 'okta-clear-client', + 'clientSecret' => 'okta-clear-secret', + 'domain' => 'before.okta.com', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('okta', [ + 'domain' => '', + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('', $response['body']['domain']); + + // Cleanup + $this->updateOAuth2('okta', [ + 'clientId' => '', + 'clientSecret' => '', + 'domain' => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Dropbox (custom param names: appKey + appSecret) // ========================================================================= @@ -1000,6 +1348,32 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2DropboxEnableAndReadBack(): void + { + $update = $this->updateOAuth2('dropbox', [ + 'appKey' => 'dropbox-enable-key', + 'appSecret' => 'dropbox-enable-secret', + 'enabled' => true, + ]); + + $this->assertSame(200, $update['headers']['status-code']); + $this->assertTrue($update['body']['enabled']); + + // GET must hide `appSecret` while keeping `appKey`. + $get = $this->getOAuth2Provider('dropbox'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertTrue($get['body']['enabled']); + $this->assertSame('dropbox-enable-key', $get['body']['appKey']); + $this->assertSame('', $get['body']['appSecret']); + + // Cleanup + $this->updateOAuth2('dropbox', [ + 'appKey' => '', + 'appSecret' => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Paypal Sandbox (inherits from Paypal — independent provider ID) // ========================================================================= @@ -1030,6 +1404,38 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2PaypalDoesNotAffectSandbox(): void + { + // Reverse direction: writing to regular paypal must leave sandbox state intact. + $this->updateOAuth2('paypalSandbox', [ + 'clientId' => 'sandbox-untouched', + 'clientSecret' => 'sandbox-secret', + 'enabled' => false, + ]); + + $this->updateOAuth2('paypal', [ + 'clientId' => 'paypal-prod', + 'secretKey' => 'paypal-prod-secret', + 'enabled' => false, + ]); + + $sandbox = $this->getOAuth2Provider('paypalSandbox'); + $this->assertSame(200, $sandbox['headers']['status-code']); + $this->assertSame('sandbox-untouched', $sandbox['body']['clientId']); + + // Cleanup both + $this->updateOAuth2('paypal', [ + 'clientId' => '', + 'secretKey' => '', + 'enabled' => false, + ]); + $this->updateOAuth2('paypalSandbox', [ + 'clientId' => '', + 'clientSecret' => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Tradeshift Sandbox (inherits from Tradeshift — independent provider ID) // ========================================================================= @@ -1060,6 +1466,38 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2TradeshiftDoesNotAffectSandbox(): void + { + // Reverse direction: writing to regular tradeshift must not touch sandbox state. + $this->updateOAuth2('tradeshiftBox', [ + 'oauth2ClientId' => 'tradeshift-sandbox-untouched', + 'oauth2ClientSecret' => 'tradeshift-sandbox-secret', + 'enabled' => false, + ]); + + $this->updateOAuth2('tradeshift', [ + 'oauth2ClientId' => 'tradeshift-prod', + 'oauth2ClientSecret' => 'tradeshift-prod-secret', + 'enabled' => false, + ]); + + $sandbox = $this->getOAuth2Provider('tradeshiftBox'); + $this->assertSame(200, $sandbox['headers']['status-code']); + $this->assertSame('tradeshift-sandbox-untouched', $sandbox['body']['oauth2ClientId']); + + // Cleanup both + $this->updateOAuth2('tradeshift', [ + 'oauth2ClientId' => '', + 'oauth2ClientSecret' => '', + 'enabled' => false, + ]); + $this->updateOAuth2('tradeshiftBox', [ + 'oauth2ClientId' => '', + 'oauth2ClientSecret' => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Smoke test: every plain (clientId + clientSecret) provider // @@ -1118,16 +1556,26 @@ trait OAuth2Base $clientId = $providerId . '-smoke-client'; $clientSecret = $providerId . '-smoke-secret'; - $response = $this->updateOAuth2($providerId, [ + $update = $this->updateOAuth2($providerId, [ $idField => $clientId, $secretField => $clientSecret, 'enabled' => false, ]); - $this->assertSame(200, $response['headers']['status-code']); - $this->assertSame($providerId, $response['body']['$id']); - $this->assertSame($clientId, $response['body'][$idField]); - $this->assertSame(false, $response['body']['enabled']); + $this->assertSame(200, $update['headers']['status-code']); + $this->assertSame($providerId, $update['body']['$id']); + $this->assertSame($clientId, $update['body'][$idField]); + $this->assertFalse($update['body']['enabled']); + + // GET round-trip — confirms the value actually persisted (catches a + // PATCH that only echoes input without writing) and that the secret + // is hidden on read. + $get = $this->getOAuth2Provider($providerId); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertSame($providerId, $get['body']['$id']); + $this->assertSame($clientId, $get['body'][$idField]); + $this->assertSame('', $get['body'][$secretField]); + $this->assertFalse($get['body']['enabled']); // Cleanup $this->updateOAuth2($providerId, [ From d0d536a2dd2a9398322307a4c17ca67a40e6c3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 17:40:49 +0200 Subject: [PATCH 40/51] Improve test coverage --- tests/e2e/Services/Project/OAuth2Base.php | 652 ++++++++++++++++++++++ 1 file changed, 652 insertions(+) diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index 215713b5b4..448ee4df59 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -154,6 +154,22 @@ trait OAuth2Base $this->assertSame(401, $response['headers']['status-code']); } + public function testListOAuth2ProvidersExcludesUnregisteredConfigEntries(): void + { + // `mock` and `mock-unverified` exist in oAuthProviders config (enabled: true) + // but are intentionally absent from Base::getProviderActions() — they're + // internal Mock OAuth2 adapters used by other test suites, not public + // providers. XList iterates the action registry, so they must never be + // included even though config marks them enabled. + $response = $this->listOAuth2Providers(); + + $this->assertSame(200, $response['headers']['status-code']); + + $ids = \array_column($response['body']['providers'], '$id'); + $this->assertNotContains('mock', $ids); + $this->assertNotContains('mock-unverified', $ids); + } + // ========================================================================= // Get OAuth2 provider // ========================================================================= @@ -209,6 +225,19 @@ trait OAuth2Base $this->assertSame('project_provider_unsupported', $response['body']['type']); } + public function testGetOAuth2ProviderRegisteredInConfigButNoUpdateClass(): void + { + // `mock` is present in oAuthProviders config (enabled: true) but is NOT + // registered in Base::getProviderActions(). Get::action has two + // separate `unsupported` throw branches — testGetOAuth2ProviderUnsupported + // covers the first (provider missing from config); this covers the + // second (provider in config but missing from the action registry). + $response = $this->getOAuth2Provider('mock'); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('project_provider_unsupported', $response['body']['type']); + } + public function testGetOAuth2ProviderWithoutAuthentication(): void { $response = $this->getOAuth2Provider('github', authenticated: false); @@ -492,6 +521,100 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2ApplePartialPreservesEachField(): void + { + // Seed all four fields, then patch each one individually and confirm + // the others survive across the chain. testUpdateOAuth2ApplePartial + // only covers `keyId`; this exercises serviceId/teamId/p8File too. + $this->updateOAuth2('apple', [ + 'serviceId' => 'ip.appwrite.app.merge', + 'keyId' => 'KEYMERGE01', + 'teamId' => 'TEAMMERGE', + 'p8File' => '-----BEGIN PRIVATE KEY-----MERGE-----END PRIVATE KEY-----', + 'enabled' => false, + ]); + + // Patch only `teamId`. + $teamOnly = $this->updateOAuth2('apple', [ + 'teamId' => 'TEAMROTATED', + ]); + $this->assertSame(200, $teamOnly['headers']['status-code']); + $this->assertSame('TEAMROTATED', $teamOnly['body']['teamId']); + $this->assertSame('ip.appwrite.app.merge', $teamOnly['body']['serviceId']); + + // Patch only `serviceId` — keyId/teamId/p8File live in the JSON blob + // and must survive a top-level (non-blob) field update. + $serviceOnly = $this->updateOAuth2('apple', [ + 'serviceId' => 'ip.appwrite.app.rotated', + ]); + $this->assertSame(200, $serviceOnly['headers']['status-code']); + $this->assertSame('ip.appwrite.app.rotated', $serviceOnly['body']['serviceId']); + + // Patch only `p8File`. keyId/teamId/serviceId must still be set + // internally — confirm by enabling. Apple has no verifyCredentials() + // hook, so persistCredentials only checks for non-empty serviceId and + // non-empty stored secret blob. + $p8Only = $this->updateOAuth2('apple', [ + 'p8File' => '-----BEGIN PRIVATE KEY-----ROTATED-----END PRIVATE KEY-----', + ]); + $this->assertSame(200, $p8Only['headers']['status-code']); + + $enable = $this->updateOAuth2('apple', ['enabled' => true]); + $this->assertSame(200, $enable['headers']['status-code']); + $this->assertTrue($enable['body']['enabled']); + + // Cleanup + $this->updateOAuth2('apple', [ + 'serviceId' => '', + 'keyId' => '', + 'teamId' => '', + 'p8File' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2AppleClearAllFieldsBlocksEnable(): void + { + // Seed all four Apple fields. + $this->updateOAuth2('apple', [ + 'serviceId' => 'ip.appwrite.app.clearAll', + 'keyId' => 'KEYCLEARALL', + 'teamId' => 'TEAMCLEARALL', + 'p8File' => '-----BEGIN PRIVATE KEY-----CLEARALL-----END PRIVATE KEY-----', + 'enabled' => false, + ]); + + // Clear all credentials with empty strings. With `enabled` omitted, the + // silent-validation branch swallows the empty-credentials throw, so the + // call still succeeds — see testUpdateOAuth2PlainEnabledOmittedDoesNotThrow. + $clear = $this->updateOAuth2('apple', [ + 'serviceId' => '', + 'keyId' => '', + 'teamId' => '', + 'p8File' => '', + ]); + $this->assertSame(200, $clear['headers']['status-code']); + $this->assertSame('', $clear['body']['serviceId']); + + // A subsequent `enabled => true` must now 400. Empty serviceId trips + // persistCredentials' empty(appId) guard before any provider hook runs, + // proving that the clear actually took effect on stored state. + $enable = $this->updateOAuth2('apple', [ + 'enabled' => true, + ]); + $this->assertSame(400, $enable['headers']['status-code']); + $this->assertSame('general_argument_invalid', $enable['body']['type']); + + // Cleanup (already cleared; included for reset symmetry). + $this->updateOAuth2('apple', [ + 'serviceId' => '', + 'keyId' => '', + 'teamId' => '', + 'p8File' => '', + 'enabled' => false, + ]); + } + public function testUpdateOAuth2AppleResponseModel(): void { $response = $this->updateOAuth2('apple', [ @@ -642,6 +765,78 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2Auth0PartialPreservesEachField(): void + { + // testUpdateOAuth2Auth0PartialEndpoint only patches `endpoint`. Cover + // patching `clientSecret` alone (must not wipe endpoint) and `clientId` + // alone (must not wipe the JSON-blob fields). + $this->updateOAuth2('auth0', [ + 'clientId' => 'auth0-merge-client', + 'clientSecret' => 'auth0-merge-secret', + 'endpoint' => 'merge.us.auth0.com', + 'enabled' => false, + ]); + + // Patch only clientSecret — clientId and endpoint must survive. + $secretOnly = $this->updateOAuth2('auth0', [ + 'clientSecret' => 'auth0-rotated-secret', + ]); + $this->assertSame(200, $secretOnly['headers']['status-code']); + $this->assertSame('auth0-merge-client', $secretOnly['body']['clientId']); + $this->assertSame('merge.us.auth0.com', $secretOnly['body']['endpoint']); + + // Patch only clientId — endpoint must survive. + $idOnly = $this->updateOAuth2('auth0', [ + 'clientId' => 'auth0-rotated-client', + ]); + $this->assertSame(200, $idOnly['headers']['status-code']); + $this->assertSame('auth0-rotated-client', $idOnly['body']['clientId']); + $this->assertSame('merge.us.auth0.com', $idOnly['body']['endpoint']); + + // Confirm the rotated clientSecret survived the chain by enabling. + // Auth0 has no verifyCredentials() hook; non-empty secret is enough. + $enable = $this->updateOAuth2('auth0', ['enabled' => true]); + $this->assertSame(200, $enable['headers']['status-code']); + $this->assertTrue($enable['body']['enabled']); + + // Cleanup + $this->updateOAuth2('auth0', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2Auth0EndpointAcceptsEmpty(): void + { + // Auth0's `endpoint` validator is `Nullable(Text(256, 0))`. Passing + // `''` must clear the stored value rather than leave it untouched + // (would happen if the merge fell back to existing on empty-string). + $this->updateOAuth2('auth0', [ + 'clientId' => 'auth0-clear-client', + 'clientSecret' => 'auth0-clear-secret', + 'endpoint' => 'before.us.auth0.com', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('auth0', [ + 'endpoint' => '', + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('', $response['body']['endpoint']); + $this->assertSame('auth0-clear-client', $response['body']['clientId']); + + // Cleanup + $this->updateOAuth2('auth0', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => '', + 'enabled' => false, + ]); + } + public function testUpdateOAuth2Auth0EnableAndReadBack(): void { $update = $this->updateOAuth2('auth0', [ @@ -687,6 +882,21 @@ trait OAuth2Base $this->assertSame('general_argument_invalid', $response['body']['type']); } + public function testUpdateOAuth2AuthentikEmptyEndpointRejected(): void + { + // The `endpoint` validator is Text(min=1). Sending `''` must be + // rejected the same way as omitting — the validator should treat the + // empty-string degenerate case as a missing required field. + $response = $this->updateOAuth2('authentik', [ + 'clientId' => 'whatever', + 'clientSecret' => 'whatever', + 'endpoint' => '', + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + } + public function testUpdateOAuth2Authentik(): void { $response = $this->updateOAuth2('authentik', [ @@ -710,6 +920,45 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2AuthentikPartialPreservesSecret(): void + { + // Authentik's `endpoint` is required on every call, so we always + // re-send it. The `clientSecret` lives in the JSON blob and must + // survive when omitted on a subsequent call that only changes clientId. + $this->updateOAuth2('authentik', [ + 'clientId' => 'authentik-merge-client', + 'clientSecret' => 'authentik-merge-secret', + 'endpoint' => 'merge.authentik.com', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('authentik', [ + 'clientId' => 'authentik-rotated-client', + 'endpoint' => 'merge.authentik.com', + ]); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('authentik-rotated-client', $response['body']['clientId']); + $this->assertSame('merge.authentik.com', $response['body']['endpoint']); + + // Confirm clientSecret survived the omitted-field merge by enabling + // — Authentik has no verifyCredentials() hook, so non-empty stored + // secret is enough. `endpoint` must be re-sent (required on enable too). + $enable = $this->updateOAuth2('authentik', [ + 'endpoint' => 'merge.authentik.com', + 'enabled' => true, + ]); + $this->assertSame(200, $enable['headers']['status-code']); + $this->assertTrue($enable['body']['enabled']); + + // Cleanup — endpoint is required, use a placeholder. + $this->updateOAuth2('authentik', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => 'cleanup.authentik.com', + 'enabled' => false, + ]); + } + public function testUpdateOAuth2AuthentikEnableAndReadBack(): void { $update = $this->updateOAuth2('authentik', [ @@ -754,6 +1003,21 @@ trait OAuth2Base $this->assertSame('general_argument_invalid', $response['body']['type']); } + public function testUpdateOAuth2MicrosoftEmptyTenantRejected(): void + { + // The `tenant` validator is Text(min=1). Sending `''` must be rejected + // the same way as omitting — the validator should treat the empty + // string as a missing required field. + $response = $this->updateOAuth2('microsoft', [ + 'applicationId' => 'whatever', + 'applicationSecret' => 'whatever', + 'tenant' => '', + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + } + public function testUpdateOAuth2Microsoft(): void { $response = $this->updateOAuth2('microsoft', [ @@ -908,6 +1172,43 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2GitlabPartialPreservesEachField(): void + { + // testUpdateOAuth2GitlabPartialEndpoint covers patching only `endpoint`. + // Cover patching `secret` alone (must not wipe applicationId/endpoint) + // and `applicationId` alone (must not wipe the JSON-blob endpoint). + $this->updateOAuth2('gitlab', [ + 'applicationId' => 'gitlab-merge-app', + 'secret' => 'gitlab-merge-secret', + 'endpoint' => 'https://merge.gitlab.com', + 'enabled' => false, + ]); + + // Patch only `secret`. + $secretOnly = $this->updateOAuth2('gitlab', [ + 'secret' => 'gitlab-rotated-secret', + ]); + $this->assertSame(200, $secretOnly['headers']['status-code']); + $this->assertSame('gitlab-merge-app', $secretOnly['body']['applicationId']); + $this->assertSame('https://merge.gitlab.com', $secretOnly['body']['endpoint']); + + // Patch only `applicationId`. + $idOnly = $this->updateOAuth2('gitlab', [ + 'applicationId' => 'gitlab-rotated-app', + ]); + $this->assertSame(200, $idOnly['headers']['status-code']); + $this->assertSame('gitlab-rotated-app', $idOnly['body']['applicationId']); + $this->assertSame('https://merge.gitlab.com', $idOnly['body']['endpoint']); + + // Cleanup + $this->updateOAuth2('gitlab', [ + 'applicationId' => '', + 'secret' => '', + 'endpoint' => '', + 'enabled' => false, + ]); + } + public function testUpdateOAuth2GitlabEnableAndReadBack(): void { $update = $this->updateOAuth2('gitlab', [ @@ -1120,6 +1421,167 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2OidcEnableInSeparateRequestWithWellKnown(): void + { + // Configure URLs first with `enabled: false`. Then enable in a SECOND + // request that omits all URL fields. The merge-on-enable logic in + // Oidc::handle() must see the previously-stored wellKnownEndpoint and + // allow the toggle. This is the headline feature of the merge logic. + $this->updateOAuth2('oidc', [ + 'clientId' => 'oidc-split-wk-client', + 'clientSecret' => 'oidc-split-wk-secret', + 'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration', + 'enabled' => false, + ]); + + $enable = $this->updateOAuth2('oidc', [ + 'enabled' => true, + ]); + $this->assertSame(200, $enable['headers']['status-code']); + $this->assertTrue($enable['body']['enabled']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2OidcEnableAcrossRequestsWithDiscoveryURLs(): void + { + // Reset to clean state — earlier tests in this section may have left + // partial URL state when running in any order. + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + + // Request 1: configure two of the three discovery URLs. + $this->updateOAuth2('oidc', [ + 'clientId' => 'oidc-split-discovery', + 'clientSecret' => 'oidc-split-discovery-secret', + 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', + 'tokenUrl' => 'https://idp.example.com/oauth2/token', + 'enabled' => false, + ]); + + // Request 2: send only the third URL plus enable=true. The merged + // state must include the two stored URLs + the new one to satisfy + // the all-three-discovery-URLs branch of the enable check. + $enable = $this->updateOAuth2('oidc', [ + 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'enabled' => true, + ]); + $this->assertSame(200, $enable['headers']['status-code']); + $this->assertTrue($enable['body']['enabled']); + + // Confirm all three URLs ended up persisted (merge wrote the new + // userInfoUrl while preserving the previously stored two). + $get = $this->getOAuth2Provider('oidc'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertSame('https://idp.example.com/oauth2/authorize', $get['body']['authorizationURL']); + $this->assertSame('https://idp.example.com/oauth2/token', $get['body']['tokenUrl']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $get['body']['userInfoUrl']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2OidcEnableFailsAfterClearingWellKnown(): void + { + // Seed wellKnownURL only (no discovery URLs). + $this->updateOAuth2('oidc', [ + 'clientId' => 'oidc-clear-then-enable', + 'clientSecret' => 'oidc-clear-then-enable-secret', + 'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + + // Clear wellKnownURL and try to enable in the same request. Merge + // sees `wellKnown=''` (the cleared empty wins over the stored value + // because the new value is non-null) and no discovery URLs → 400. + // This is the inverse of testUpdateOAuth2OidcEnableInSeparateRequestWithWellKnown: + // confirms the merge correctly *replaces* with empty rather than + // falling back to the stored non-empty value. + $response = $this->updateOAuth2('oidc', [ + 'wellKnownURL' => '', + 'enabled' => true, + ]); + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2OidcSwitchModesWellKnownToDiscovery(): void + { + // Configure with wellKnownURL, then switch to the three-discovery-URL + // mode in a single request: clear wellKnown, set the three URLs, + // enable. Merge sees wellKnown='' AND all three discovery URLs set → + // hasAllDiscovery branch passes. + $this->updateOAuth2('oidc', [ + 'clientId' => 'oidc-switch-client', + 'clientSecret' => 'oidc-switch-secret', + 'wellKnownURL' => 'https://idp.example.com/.well-known/openid-configuration', + 'enabled' => false, + ]); + + $switch = $this->updateOAuth2('oidc', [ + 'wellKnownURL' => '', + 'authorizationURL' => 'https://idp.example.com/oauth2/authorize', + 'tokenUrl' => 'https://idp.example.com/oauth2/token', + 'userInfoUrl' => 'https://idp.example.com/oauth2/userinfo', + 'enabled' => true, + ]); + $this->assertSame(200, $switch['headers']['status-code']); + $this->assertTrue($switch['body']['enabled']); + $this->assertSame('', $switch['body']['wellKnownURL']); + $this->assertSame('https://idp.example.com/oauth2/authorize', $switch['body']['authorizationURL']); + $this->assertSame('https://idp.example.com/oauth2/token', $switch['body']['tokenUrl']); + $this->assertSame('https://idp.example.com/oauth2/userinfo', $switch['body']['userInfoUrl']); + + // Cleanup + $this->updateOAuth2('oidc', [ + 'clientId' => '', + 'clientSecret' => '', + 'wellKnownURL' => '', + 'authorizationURL' => '', + 'tokenUrl' => '', + 'userInfoUrl' => '', + 'enabled' => false, + ]); + } + public function testUpdateOAuth2OidcURLsAcceptEmpty(): void { // All four URL fields use `Nullable(URL(empty: true))`. Passing `''` @@ -1256,6 +1718,90 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2OktaPartialPreservesEachField(): void + { + // Okta has no field-by-field partial test in the existing suite. Cover + // each of `domain`, `authorizationServerId`, and `clientSecret` being + // patched alone — all three live in the same JSON blob. + $this->updateOAuth2('okta', [ + 'clientId' => 'okta-merge-client', + 'clientSecret' => 'okta-merge-secret', + 'domain' => 'merge.okta.com', + 'authorizationServerId' => 'aus000000000000merge', + 'enabled' => false, + ]); + + // Patch only `domain` — others must survive. + $domainOnly = $this->updateOAuth2('okta', [ + 'domain' => 'rotated.okta.com', + ]); + $this->assertSame(200, $domainOnly['headers']['status-code']); + $this->assertSame('rotated.okta.com', $domainOnly['body']['domain']); + $this->assertSame('okta-merge-client', $domainOnly['body']['clientId']); + $this->assertSame('aus000000000000merge', $domainOnly['body']['authorizationServerId']); + + // Patch only `authorizationServerId`. + $authServerOnly = $this->updateOAuth2('okta', [ + 'authorizationServerId' => 'aus000000000rotated00', + ]); + $this->assertSame(200, $authServerOnly['headers']['status-code']); + $this->assertSame('rotated.okta.com', $authServerOnly['body']['domain']); + $this->assertSame('aus000000000rotated00', $authServerOnly['body']['authorizationServerId']); + + // Patch only `clientSecret` — domain and authServerId in the JSON blob + // must survive. Confirm the rotated secret persisted by enabling. + $secretOnly = $this->updateOAuth2('okta', [ + 'clientSecret' => 'okta-rotated-secret', + ]); + $this->assertSame(200, $secretOnly['headers']['status-code']); + $this->assertSame('rotated.okta.com', $secretOnly['body']['domain']); + $this->assertSame('aus000000000rotated00', $secretOnly['body']['authorizationServerId']); + + $enable = $this->updateOAuth2('okta', ['enabled' => true]); + $this->assertSame(200, $enable['headers']['status-code']); + $this->assertTrue($enable['body']['enabled']); + + // Cleanup + $this->updateOAuth2('okta', [ + 'clientId' => '', + 'clientSecret' => '', + 'domain' => '', + 'authorizationServerId' => '', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2OktaAuthServerIdAcceptsEmpty(): void + { + // `authorizationServerId` is `Nullable(Text(256, 0))`. Passing `''` + // must clear the stored value while leaving the rest of the JSON blob + // (clientSecret, oktaDomain) untouched. + $this->updateOAuth2('okta', [ + 'clientId' => 'okta-clear-auth-server', + 'clientSecret' => 'okta-clear-auth-server-secret', + 'domain' => 'authserver.okta.com', + 'authorizationServerId' => 'aus0000000000beforeauth', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('okta', [ + 'authorizationServerId' => '', + ]); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('', $response['body']['authorizationServerId']); + // domain (also stored in the JSON blob) must NOT have been wiped. + $this->assertSame('authserver.okta.com', $response['body']['domain']); + + // Cleanup + $this->updateOAuth2('okta', [ + 'clientId' => '', + 'clientSecret' => '', + 'domain' => '', + 'authorizationServerId' => '', + 'enabled' => false, + ]); + } + public function testUpdateOAuth2OktaDomainAcceptsEmpty(): void { // The `domain` validator is `Nullable(Domain(empty: true))`. Passing @@ -1404,6 +1950,34 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2PaypalSandboxResponseModel(): void + { + // PaypalSandbox inherits from Paypal: param/response field is + // `secretKey` instead of `clientSecret`. A regression that adds the + // default `clientSecret` to the response model would leak the + // unwritten field; pin its absence on both PATCH and GET. + $update = $this->updateOAuth2('paypalSandbox', [ + 'clientId' => 'paypal-sandbox-shape', + 'secretKey' => 'paypal-sandbox-shape-secret', + 'enabled' => false, + ]); + $this->assertSame(200, $update['headers']['status-code']); + $this->assertArrayHasKey('secretKey', $update['body']); + $this->assertArrayNotHasKey('clientSecret', $update['body']); + + $get = $this->getOAuth2Provider('paypalSandbox'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertArrayHasKey('secretKey', $get['body']); + $this->assertArrayNotHasKey('clientSecret', $get['body']); + + // Cleanup + $this->updateOAuth2('paypalSandbox', [ + 'clientId' => '', + 'secretKey' => '', + 'enabled' => false, + ]); + } + public function testUpdateOAuth2PaypalDoesNotAffectSandbox(): void { // Reverse direction: writing to regular paypal must leave sandbox state intact. @@ -1466,6 +2040,38 @@ trait OAuth2Base ]); } + public function testUpdateOAuth2TradeshiftBoxResponseModel(): void + { + // TradeshiftSandbox inherits from Tradeshift: both clientId AND + // clientSecret are renamed (oauth2ClientId / oauth2ClientSecret). + // Pin that the default field names are absent from PATCH and GET + // responses so a stray addition to the response model is caught. + $update = $this->updateOAuth2('tradeshiftBox', [ + 'oauth2ClientId' => 'tradeshift-box-shape', + 'oauth2ClientSecret' => 'tradeshift-box-shape-secret', + 'enabled' => false, + ]); + $this->assertSame(200, $update['headers']['status-code']); + $this->assertArrayHasKey('oauth2ClientId', $update['body']); + $this->assertArrayHasKey('oauth2ClientSecret', $update['body']); + $this->assertArrayNotHasKey('clientId', $update['body']); + $this->assertArrayNotHasKey('clientSecret', $update['body']); + + $get = $this->getOAuth2Provider('tradeshiftBox'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertArrayHasKey('oauth2ClientId', $get['body']); + $this->assertArrayHasKey('oauth2ClientSecret', $get['body']); + $this->assertArrayNotHasKey('clientId', $get['body']); + $this->assertArrayNotHasKey('clientSecret', $get['body']); + + // Cleanup + $this->updateOAuth2('tradeshiftBox', [ + 'oauth2ClientId' => '', + 'oauth2ClientSecret' => '', + 'enabled' => false, + ]); + } + public function testUpdateOAuth2TradeshiftDoesNotAffectSandbox(): void { // Reverse direction: writing to regular tradeshift must not touch sandbox state. @@ -1585,6 +2191,52 @@ trait OAuth2Base ]); } + /** + * For providers that rename `clientId` / `clientSecret` to a custom field + * (e.g. `apiKey`/`apiSecret`, `customerKey`/`secretKey`, `oauthClientId`), + * the renamed field replaces the default — the response model must NOT + * also expose the default name. Catches a regression where adding a + * custom param name forgets to remove the default from the response. + */ + #[DataProvider('plainProviders')] + public function testUpdateOAuth2PlainProviderResponseDoesNotLeakDefaultNames(string $providerId, string $idField, string $secretField): void + { + if ($idField === 'clientId' && $secretField === 'clientSecret') { + // Default-named provider — nothing to leak. Avoids a no-op assertion. + $this->markTestSkipped("{$providerId} uses default field names."); + } + + $update = $this->updateOAuth2($providerId, [ + $idField => $providerId . '-leak-check-id', + $secretField => $providerId . '-leak-check-secret', + 'enabled' => false, + ]); + $this->assertSame(200, $update['headers']['status-code']); + + if ($idField !== 'clientId') { + $this->assertArrayNotHasKey('clientId', $update['body'], "PATCH response for {$providerId} leaks default `clientId` despite using `{$idField}`."); + } + if ($secretField !== 'clientSecret') { + $this->assertArrayNotHasKey('clientSecret', $update['body'], "PATCH response for {$providerId} leaks default `clientSecret` despite using `{$secretField}`."); + } + + $get = $this->getOAuth2Provider($providerId); + $this->assertSame(200, $get['headers']['status-code']); + if ($idField !== 'clientId') { + $this->assertArrayNotHasKey('clientId', $get['body'], "GET response for {$providerId} leaks default `clientId` despite using `{$idField}`."); + } + if ($secretField !== 'clientSecret') { + $this->assertArrayNotHasKey('clientSecret', $get['body'], "GET response for {$providerId} leaks default `clientSecret` despite using `{$secretField}`."); + } + + // Cleanup + $this->updateOAuth2($providerId, [ + $idField => '', + $secretField => '', + 'enabled' => false, + ]); + } + // ========================================================================= // Helpers // ========================================================================= From 3d43530225ae403545cd5c33010baf1b45694462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 17:41:13 +0200 Subject: [PATCH 41/51] Fix failing test --- tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php b/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php index 58123aeff3..f86557a432 100644 --- a/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php +++ b/tests/e2e/Services/Project/OAuthGitHubIntegrationTest.php @@ -80,7 +80,7 @@ class OAuthGitHubIntegrationTest extends Scope $this->assertNotNull($githubProvider, 'GitHub OAuth provider not found in project details'); $this->assertTrue($githubProvider['enabled']); $this->assertSame($clientId, $githubProvider['appId']); - $this->assertSame($clientSecret, $githubProvider['secret']); + $this->assertSame('', $githubProvider['secret']); // Write only // Step 5: Without client headers (no API key), go through the OAuth flow $clientHeaders = [ From 50d86c5b5dafdcc67565c8df863d0619b45ec775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 17:45:52 +0200 Subject: [PATCH 42/51] Update ci.yml --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d28c00477a..e521ac3771 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -456,8 +456,10 @@ jobs: name: ${{ env.IMAGE }} path: /tmp - - name: Set database environment + - name: Set environment run: | + echo "_APP_OPTIONS_ROUTER_PROTECTION=enabled" >> $GITHUB_ENV + if [ "${{ matrix.database }}" = "MariaDB" ]; then echo "COMPOSE_PROFILES=mariadb" >> $GITHUB_ENV echo "_APP_DB_ADAPTER=mariadb" >> $GITHUB_ENV From 015aee087a8640c7e8149f047a90dac94bb2d088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 18:04:22 +0200 Subject: [PATCH 43/51] Fix write only security --- .../Http/Project/OAuth2/Apple/Update.php | 18 +++-------------- .../Http/Project/OAuth2/Auth0/Update.php | 17 +++------------- .../Http/Project/OAuth2/Authentik/Update.php | 17 +++------------- .../Project/Http/Project/OAuth2/Base.php | 14 ++++--------- .../Http/Project/OAuth2/Gitlab/Update.php | 17 +++------------- .../Http/Project/OAuth2/Microsoft/Update.php | 17 +++------------- .../Http/Project/OAuth2/Oidc/Update.php | 20 +++---------------- .../Http/Project/OAuth2/Okta/Update.php | 18 +++-------------- tests/e2e/Services/Project/OAuth2Base.php | 17 +++++++++++----- 9 files changed, 37 insertions(+), 118 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php index 79a30e02d4..c2b0885f5f 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php @@ -158,20 +158,8 @@ class Update extends Base $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $serviceId, $encodedSecret, $enabled); - $oAuthProviders = $project->getAttribute('oAuthProviders', []); - $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; - $decoded = []; - if (!empty($storedRaw)) { - $decoded = \json_decode($storedRaw, true) ?: []; - } - - $response->dynamic(new Document([ - '$id' => $providerId, - 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, - static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', - 'keyId' => $decoded['keyID'] ?? '', - 'teamId' => $decoded['teamID'] ?? '', - 'p8File' => $decoded['p8'] ?? '', - ]), static::getResponseModel()); + // Reuse buildReadResponse to keep PATCH/GET shapes identical and + // guarantee keyId/teamId/p8File are write-only on every response path. + $response->dynamic($this->buildReadResponse($project), static::getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php index 4cb314af13..9c94864a50 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php @@ -146,19 +146,8 @@ class Update extends Base $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled); - $oAuthProviders = $project->getAttribute('oAuthProviders', []); - $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; - $decoded = []; - if (!empty($storedRaw)) { - $decoded = \json_decode($storedRaw, true) ?: []; - } - - $response->dynamic(new Document([ - '$id' => $providerId, - 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, - static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', - static::getClientSecretParamName() => $decoded['clientSecret'] ?? '', - 'endpoint' => $decoded['auth0Domain'] ?? '', - ]), static::getResponseModel()); + // Reuse buildReadResponse to keep PATCH/GET shapes identical and + // guarantee the clientSecret is write-only on every response path. + $response->dynamic($this->buildReadResponse($project), static::getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php index 834a68597a..c4e27899a8 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php @@ -143,19 +143,8 @@ class Update extends Base $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled); - $oAuthProviders = $project->getAttribute('oAuthProviders', []); - $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; - $decoded = []; - if (!empty($storedRaw)) { - $decoded = \json_decode($storedRaw, true) ?: []; - } - - $response->dynamic(new Document([ - '$id' => $providerId, - 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, - static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', - static::getClientSecretParamName() => $decoded['clientSecret'] ?? '', - 'endpoint' => $decoded['authentikDomain'] ?? '', - ]), static::getResponseModel()); + // Reuse buildReadResponse to keep PATCH/GET shapes identical and + // guarantee the clientSecret is write-only on every response path. + $response->dynamic($this->buildReadResponse($project), static::getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php index f5aa5a34cd..6591270ded 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php @@ -311,16 +311,10 @@ abstract class Base extends Action ): void { $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $clientSecret, $enabled); - $providerId = static::getProviderId(); - $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $queueForEvents->setParam('providerId', static::getProviderId()); - $queueForEvents->setParam('providerId', $providerId); - - $response->dynamic(new Document([ - '$id' => $providerId, - 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, - static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', - static::getClientSecretParamName() => $oAuthProviders[$providerId . 'Secret'] ?? '', - ]), static::getResponseModel()); + // Reuse buildReadResponse to keep PATCH/GET shapes identical and + // guarantee the clientSecret is write-only on every response path. + $response->dynamic($this->buildReadResponse($project), static::getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php index e860046b25..743ffa5061 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php @@ -157,19 +157,8 @@ class Update extends Base $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $applicationId, $encodedSecret, $enabled); - $oAuthProviders = $project->getAttribute('oAuthProviders', []); - $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; - $decoded = []; - if (!empty($storedRaw)) { - $decoded = \json_decode($storedRaw, true) ?: []; - } - - $response->dynamic(new Document([ - '$id' => $providerId, - 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, - static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', - static::getClientSecretParamName() => $decoded['clientSecret'] ?? '', - 'endpoint' => $decoded['endpoint'] ?? '', - ]), static::getResponseModel()); + // Reuse buildReadResponse to keep PATCH/GET shapes identical and + // guarantee the secret is write-only on every response path. + $response->dynamic($this->buildReadResponse($project), static::getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php index 894631fbaa..5f72b65dd8 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php @@ -153,19 +153,8 @@ class Update extends Base $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $applicationId, $encodedSecret, $enabled); - $oAuthProviders = $project->getAttribute('oAuthProviders', []); - $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; - $decoded = []; - if (!empty($storedRaw)) { - $decoded = \json_decode($storedRaw, true) ?: []; - } - - $response->dynamic(new Document([ - '$id' => $providerId, - 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, - static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', - static::getClientSecretParamName() => $decoded['clientSecret'] ?? '', - 'tenant' => $decoded['tenantID'] ?? '', - ]), static::getResponseModel()); + // Reuse buildReadResponse to keep PATCH/GET shapes identical and + // guarantee the applicationSecret is write-only on every response path. + $response->dynamic($this->buildReadResponse($project), static::getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php index 2fda493b2f..95d06c5da9 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php @@ -183,22 +183,8 @@ class Update extends Base $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled); - $oAuthProviders = $project->getAttribute('oAuthProviders', []); - $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; - $decoded = []; - if (!empty($storedRaw)) { - $decoded = \json_decode($storedRaw, true) ?: []; - } - - $response->dynamic(new Document([ - '$id' => $providerId, - 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, - static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', - static::getClientSecretParamName() => $decoded['clientSecret'] ?? '', - 'wellKnownURL' => $decoded['wellKnownEndpoint'] ?? '', - 'authorizationURL' => $decoded['authorizationEndpoint'] ?? '', - 'tokenUrl' => $decoded['tokenEndpoint'] ?? '', - 'userInfoUrl' => $decoded['userInfoEndpoint'] ?? '', - ]), static::getResponseModel()); + // Reuse buildReadResponse to keep PATCH/GET shapes identical and + // guarantee the clientSecret is write-only on every response path. + $response->dynamic($this->buildReadResponse($project), static::getResponseModel()); } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php index 9f5f2d6307..bc8583c086 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php @@ -163,20 +163,8 @@ class Update extends Base $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled); - $oAuthProviders = $project->getAttribute('oAuthProviders', []); - $storedRaw = $oAuthProviders[$providerId . 'Secret'] ?? ''; - $decoded = []; - if (!empty($storedRaw)) { - $decoded = \json_decode($storedRaw, true) ?: []; - } - - $response->dynamic(new Document([ - '$id' => $providerId, - 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, - static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', - static::getClientSecretParamName() => $decoded['clientSecret'] ?? '', - 'domain' => $decoded['oktaDomain'] ?? '', - 'authorizationServerId' => $decoded['authorizationServerId'] ?? '', - ]), static::getResponseModel()); + // Reuse buildReadResponse to keep PATCH/GET shapes identical and + // guarantee the clientSecret is write-only on every response path. + $response->dynamic($this->buildReadResponse($project), static::getResponseModel()); } } diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index 448ee4df59..f33fc7acb0 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -476,8 +476,10 @@ trait OAuth2Base $this->assertSame(200, $response['headers']['status-code']); $this->assertSame('apple', $response['body']['$id']); $this->assertSame('ip.appwrite.app.web', $response['body']['serviceId']); - $this->assertSame('P4000000N8', $response['body']['keyId']); - $this->assertSame('D4000000R6', $response['body']['teamId']); + // keyId / teamId / p8File are write-only — PATCH response must not echo them back. + $this->assertSame('', $response['body']['keyId']); + $this->assertSame('', $response['body']['teamId']); + $this->assertSame('', $response['body']['p8File']); $this->assertSame(false, $response['body']['enabled']); // Cleanup @@ -507,9 +509,12 @@ trait OAuth2Base ]); $this->assertSame(200, $response['headers']['status-code']); - $this->assertSame('KEYUPDATED', $response['body']['keyId']); - $this->assertSame('TEAMSEED01', $response['body']['teamId']); + // serviceId is the (non-secret) clientId; keyId/teamId are write-only + // and must not surface in the response. Persistence of the merged + // values is verified separately via the enable-after-merge tests. $this->assertSame('ip.appwrite.app.seed', $response['body']['serviceId']); + $this->assertSame('', $response['body']['keyId']); + $this->assertSame('', $response['body']['teamId']); // Cleanup $this->updateOAuth2('apple', [ @@ -539,7 +544,9 @@ trait OAuth2Base 'teamId' => 'TEAMROTATED', ]); $this->assertSame(200, $teamOnly['headers']['status-code']); - $this->assertSame('TEAMROTATED', $teamOnly['body']['teamId']); + // teamId is write-only; verify only the non-secret serviceId echo. + // The actual merge is validated by the enable-after-merge call below. + $this->assertSame('', $teamOnly['body']['teamId']); $this->assertSame('ip.appwrite.app.merge', $teamOnly['body']['serviceId']); // Patch only `serviceId` — keyId/teamId/p8File live in the JSON blob From 1f16b0d9e759a6b8bcec1d02971d52ef11930fb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 18:21:21 +0200 Subject: [PATCH 44/51] Fix failing startup --- composer.json | 1 + composer.lock | 174 +++++++++--------- .../Http/Project/OAuth2/Gitlab/Update.php | 2 +- .../Http/Project/OAuth2/Oidc/Update.php | 8 +- .../Http/Project/OAuth2/Okta/Update.php | 2 +- tests/e2e/Services/Project/OAuth2Base.php | 6 +- 6 files changed, 98 insertions(+), 95 deletions(-) diff --git a/composer.json b/composer.json index 6312243e32..b5ca436c3f 100644 --- a/composer.json +++ b/composer.json @@ -69,6 +69,7 @@ "utopia-php/dsn": "0.2.1", "utopia-php/http": "0.34.*", "utopia-php/fetch": "0.5.*", + "utopia-php/validators": "0.2.*", "utopia-php/image": "0.8.*", "utopia-php/locale": "0.8.*", "utopia-php/logger": "0.6.*", diff --git a/composer.lock b/composer.lock index 02590020e0..82b705a5c7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c5ae97637fd0ec0a950044d1c33677ea", + "content-hash": "805802552f7482eaeae4bdaa505ae982", "packages": [ { "name": "adhocore/jwt", @@ -1996,16 +1996,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "3.0.51", + "version": "3.0.52", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "d59c94077f9c9915abb51ddb52ce85188ece1748" + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d59c94077f9c9915abb51ddb52ce85188ece1748", - "reference": "d59c94077f9c9915abb51ddb52ce85188ece1748", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce", "shasum": "" }, "require": { @@ -2086,7 +2086,7 @@ ], "support": { "issues": "https://github.com/phpseclib/phpseclib/issues", - "source": "https://github.com/phpseclib/phpseclib/tree/3.0.51" + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.52" }, "funding": [ { @@ -2102,7 +2102,7 @@ "type": "tidelift" } ], - "time": "2026-04-10T01:33:53+00:00" + "time": "2026-04-27T07:02:15+00:00" }, { "name": "psr/clock", @@ -2887,7 +2887,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -2948,7 +2948,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" }, "funding": [ { @@ -2972,7 +2972,7 @@ }, { "name": "symfony/polyfill-php82", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php82.git", @@ -3028,7 +3028,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php82/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-php82/tree/v1.37.0" }, "funding": [ { @@ -3052,7 +3052,7 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", @@ -3108,7 +3108,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.37.0" }, "funding": [ { @@ -3132,16 +3132,16 @@ }, { "name": "symfony/polyfill-php85", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "2c408a6bb0313e6001a83628dc5506100474254e" + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e", - "reference": "2c408a6bb0313e6001a83628dc5506100474254e", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", "shasum": "" }, "require": { @@ -3188,7 +3188,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0" }, "funding": [ { @@ -3208,7 +3208,7 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:50:15+00:00" + "time": "2026-04-26T13:10:57+00:00" }, { "name": "symfony/service-contracts", @@ -3658,16 +3658,16 @@ }, { "name": "utopia-php/cli", - "version": "0.23.1", + "version": "0.23.2", "source": { "type": "git", "url": "https://github.com/utopia-php/cli.git", - "reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621" + "reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cli/zipball/8d1955b8bc4dc631f45d7c7df689ed7b63f70621", - "reference": "8d1955b8bc4dc631f45d7c7df689ed7b63f70621", + "url": "https://api.github.com/repos/utopia-php/cli/zipball/145b91fef827853bcceaa3ab8ca2b1d6faaca2ab", + "reference": "145b91fef827853bcceaa3ab8ca2b1d6faaca2ab", "shasum": "" }, "require": { @@ -3703,9 +3703,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cli/issues", - "source": "https://github.com/utopia-php/cli/tree/0.23.1" + "source": "https://github.com/utopia-php/cli/tree/0.23.2" }, - "time": "2026-04-05T15:27:35+00:00" + "time": "2026-04-27T09:19:04+00:00" }, { "name": "utopia-php/compression", @@ -4271,21 +4271,20 @@ }, { "name": "utopia-php/http", - "version": "0.34.21", + "version": "0.34.24", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "49a6bd3ea0d2966aa19cf707255d442675288a24" + "reference": "d1eced0627c5a9fceddf53992ed97d664b810d33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/49a6bd3ea0d2966aa19cf707255d442675288a24", - "reference": "49a6bd3ea0d2966aa19cf707255d442675288a24", + "url": "https://api.github.com/repos/utopia-php/http/zipball/d1eced0627c5a9fceddf53992ed97d664b810d33", + "reference": "d1eced0627c5a9fceddf53992ed97d664b810d33", "shasum": "" }, "require": { - "ext-swoole": "*", - "php": ">=8.2", + "php": ">=8.3", "utopia-php/compression": "0.1.*", "utopia-php/di": "0.3.*", "utopia-php/servers": "0.3.*", @@ -4295,11 +4294,14 @@ "require-dev": { "doctrine/instantiator": "^1.5", "laravel/pint": "1.*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "1.*", - "phpunit/phpunit": "^9.5.25", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^12.0", + "rector/rector": "^2.4", "swoole/ide-helper": "4.8.3" }, + "suggest": { + "ext-swoole": "Required to use the Swoole server adapter (\\Utopia\\Http\\Adapter\\Swoole\\Server)." + }, "type": "library", "autoload": { "psr-4": { @@ -4319,9 +4321,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.34.21" + "source": "https://github.com/utopia-php/http/tree/0.34.24" }, - "time": "2026-04-19T19:44:04+00:00" + "time": "2026-04-24T12:16:53+00:00" }, { "name": "utopia-php/image", @@ -4528,16 +4530,16 @@ }, { "name": "utopia-php/migration", - "version": "1.9.1", + "version": "1.9.3", "source": { "type": "git", "url": "https://github.com/utopia-php/migration.git", - "reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2" + "reference": "111f6221d04578a6f721c23ac872002375f176ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/migration/zipball/7a86aeadf182b63a9f4ceba7e137588b31c5d2e2", - "reference": "7a86aeadf182b63a9f4ceba7e137588b31c5d2e2", + "url": "https://api.github.com/repos/utopia-php/migration/zipball/111f6221d04578a6f721c23ac872002375f176ae", + "reference": "111f6221d04578a6f721c23ac872002375f176ae", "shasum": "" }, "require": { @@ -4577,22 +4579,22 @@ ], "support": { "issues": "https://github.com/utopia-php/migration/issues", - "source": "https://github.com/utopia-php/migration/tree/1.9.1" + "source": "https://github.com/utopia-php/migration/tree/1.9.3" }, - "time": "2026-03-25T07:05:27+00:00" + "time": "2026-04-22T07:13:26+00:00" }, { "name": "utopia-php/mongo", - "version": "1.0.2", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/utopia-php/mongo.git", - "reference": "677a21c53f7a1316c528b4b45b3fce886cee7223" + "reference": "73593682deee4696525a04e26524c1c1226e1530" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/mongo/zipball/677a21c53f7a1316c528b4b45b3fce886cee7223", - "reference": "677a21c53f7a1316c528b4b45b3fce886cee7223", + "url": "https://api.github.com/repos/utopia-php/mongo/zipball/73593682deee4696525a04e26524c1c1226e1530", + "reference": "73593682deee4696525a04e26524c1c1226e1530", "shasum": "" }, "require": { @@ -4638,9 +4640,9 @@ ], "support": { "issues": "https://github.com/utopia-php/mongo/issues", - "source": "https://github.com/utopia-php/mongo/tree/1.0.2" + "source": "https://github.com/utopia-php/mongo/tree/1.1.0" }, - "time": "2026-03-18T02:45:50+00:00" + "time": "2026-04-24T06:15:10+00:00" }, { "name": "utopia-php/platform", @@ -5182,16 +5184,16 @@ }, { "name": "utopia-php/validators", - "version": "0.2.0", + "version": "0.2.1", "source": { "type": "git", "url": "https://github.com/utopia-php/validators.git", - "reference": "30b6030a5b100fc1dff34506e5053759594b2a20" + "reference": "6cce9f73aa79f30de54aa3ff117090af570027cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/validators/zipball/30b6030a5b100fc1dff34506e5053759594b2a20", - "reference": "30b6030a5b100fc1dff34506e5053759594b2a20", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/6cce9f73aa79f30de54aa3ff117090af570027cb", + "reference": "6cce9f73aa79f30de54aa3ff117090af570027cb", "shasum": "" }, "require": { @@ -5221,9 +5223,9 @@ ], "support": { "issues": "https://github.com/utopia-php/validators/issues", - "source": "https://github.com/utopia-php/validators/tree/0.2.0" + "source": "https://github.com/utopia-php/validators/tree/0.2.1" }, - "time": "2026-01-13T09:16:51+00:00" + "time": "2026-04-27T16:05:19+00:00" }, { "name": "utopia-php/vcs", @@ -5464,16 +5466,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "1.20", + "version": "1.24.0", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "525f0630520c95100fcdfb63c9dac859c1d02588" + "reference": "6d4d26659bc7a1c347c1d4d8dae3b77b5562e0cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/525f0630520c95100fcdfb63c9dac859c1d02588", - "reference": "525f0630520c95100fcdfb63c9dac859c1d02588", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6d4d26659bc7a1c347c1d4d8dae3b77b5562e0cb", + "reference": "6d4d26659bc7a1c347c1d4d8dae3b77b5562e0cb", "shasum": "" }, "require": { @@ -5509,9 +5511,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/1.20" + "source": "https://github.com/appwrite/sdk-generator/tree/1.24.0" }, - "time": "2026-04-20T05:45:00+00:00" + "time": "2026-04-24T12:50:05+00:00" }, { "name": "brianium/paratest", @@ -5793,16 +5795,16 @@ }, { "name": "laravel/pint", - "version": "v1.29.0", + "version": "v1.29.1", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "bdec963f53172c5e36330f3a400604c69bf02d39" + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/bdec963f53172c5e36330f3a400604c69bf02d39", - "reference": "bdec963f53172c5e36330f3a400604c69bf02d39", + "url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80", + "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80", "shasum": "" }, "require": { @@ -5813,14 +5815,14 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.94.2", - "illuminate/view": "^12.54.1", - "larastan/larastan": "^3.9.3", - "laravel-zero/framework": "^12.0.5", + "friendsofphp/php-cs-fixer": "^3.95.1", + "illuminate/view": "^12.56.0", + "larastan/larastan": "^3.9.6", + "laravel-zero/framework": "^12.1.0", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.4.0", "pestphp/pest": "^3.8.6", - "shipfastlabs/agent-detector": "^1.1.0" + "shipfastlabs/agent-detector": "^1.1.3" }, "bin": [ "builds/pint" @@ -5857,7 +5859,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-03-12T15:51:39+00:00" + "time": "2026-04-20T15:26:14+00:00" }, { "name": "matthiasmullie/minify", @@ -6220,11 +6222,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.50", + "version": "2.1.51", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d452086fb4cf648c6b2d8cf3b639351f79e4f3e2", - "reference": "d452086fb4cf648c6b2d8cf3b639351f79e4f3e2", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59", + "reference": "dc3b523c45e714c70de2ac5113b958223b55dc59", "shasum": "" }, "require": { @@ -6269,7 +6271,7 @@ "type": "github" } ], - "time": "2026-04-17T13:10:32+00:00" + "time": "2026-04-21T18:22:01+00:00" }, { "name": "phpunit/php-code-coverage", @@ -7779,7 +7781,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -7838,7 +7840,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -7862,16 +7864,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df" + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df", - "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", "shasum": "" }, "require": { @@ -7920,7 +7922,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" }, "funding": [ { @@ -7940,11 +7942,11 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-04-26T13:13:48+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -8005,7 +8007,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" }, "funding": [ { @@ -8029,7 +8031,7 @@ }, { "name": "symfony/polyfill-php81", - "version": "v1.36.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php81.git", @@ -8085,7 +8087,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.36.0" + "source": "https://github.com/symfony/polyfill-php81/tree/v1.37.0" }, "funding": [ { diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php index 743ffa5061..70c538454f 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php @@ -94,7 +94,7 @@ class Update extends Base )) ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) - ->param('endpoint', null, new Nullable(new URL(empty: true)), 'Endpoint URL of self-hosted GitLab instance. For example: https://gitlab.com', optional: true) + ->param('endpoint', null, new Nullable(new URL(allowEmpty: true)), 'Endpoint URL of self-hosted GitLab instance. For example: https://gitlab.com', 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') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php index 95d06c5da9..c000b456ec 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php @@ -85,10 +85,10 @@ class Update extends Base )) ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) - ->param('wellKnownURL', null, new Nullable(new URL(empty: true)), 'OpenID Connect well-known configuration URL. When provided, authorization, token, and user info endpoints can be discovered automatically. For example: https://myoauth.com/.well-known/openid-configuration', optional: true) - ->param('authorizationURL', null, new Nullable(new URL(empty: true)), 'OpenID Connect authorization endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/authorize', optional: true) - ->param('tokenUrl', null, new Nullable(new URL(empty: true)), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true) - ->param('userInfoUrl', null, new Nullable(new URL(empty: true)), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', optional: true) + ->param('wellKnownURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect well-known configuration URL. When provided, authorization, token, and user info endpoints can be discovered automatically. For example: https://myoauth.com/.well-known/openid-configuration', optional: true) + ->param('authorizationURL', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect authorization endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/authorize', optional: true) + ->param('tokenUrl', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect token endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/token', optional: true) + ->param('userInfoUrl', null, new Nullable(new URL(allowEmpty: true)), 'OpenID Connect user info endpoint URL. Required when wellKnownURL is not provided. For example: https://myoauth.com/oauth2/userinfo', 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') diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php index bc8583c086..504c0636af 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php @@ -85,7 +85,7 @@ class Update extends Base )) ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) - ->param('domain', null, new Nullable(new ValidatorDomain(empty: true)), 'Okta company domain. Required when enabling the provider. For example: trial-6400025.okta.com. Example of wrong value: trial-6400025-admin.okta.com, or https://trial-6400025.okta.com/', optional: true) + ->param('domain', null, new Nullable(new ValidatorDomain(allowEmpty: true)), 'Okta company domain. Required when enabling the provider. For example: trial-6400025.okta.com. Example of wrong value: trial-6400025-admin.okta.com, or https://trial-6400025.okta.com/', optional: true) ->param('authorizationServerId', null, new Nullable(new Text(256, 0)), 'Custom Authorization Servers. Optional, can be left empty or unconfigured. For example: aus000000000000000h7z', 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') diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index f33fc7acb0..ec070531e7 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -1247,7 +1247,7 @@ trait OAuth2Base public function testUpdateOAuth2GitlabEndpointAcceptsEmpty(): void { - // The `endpoint` validator is `Nullable(URL(empty: true))`. Passing + // The `endpoint` validator is `Nullable(URL(allowEmpty: true))`. Passing // `''` must clear the stored value rather than 400 on URL validation. $this->updateOAuth2('gitlab', [ 'applicationId' => 'gitlab-clear-app', @@ -1591,7 +1591,7 @@ trait OAuth2Base public function testUpdateOAuth2OidcURLsAcceptEmpty(): void { - // All four URL fields use `Nullable(URL(empty: true))`. Passing `''` + // All four URL fields use `Nullable(URL(allowEmpty: true))`. Passing `''` // for each must clear them rather than 400 on URL validation. $this->updateOAuth2('oidc', [ 'clientId' => 'oidc-clear-client', @@ -1811,7 +1811,7 @@ trait OAuth2Base public function testUpdateOAuth2OktaDomainAcceptsEmpty(): void { - // The `domain` validator is `Nullable(Domain(empty: true))`. Passing + // The `domain` validator is `Nullable(Domain(allowEmpty: true))`. Passing // `''` must clear the stored value rather than 400 on Domain validation. $this->updateOAuth2('okta', [ 'clientId' => 'okta-clear-client', From ad4178aa42b2c236b6e6f4ec6f905b823d63ced1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 Apr 2026 18:33:30 +0200 Subject: [PATCH 45/51] Fix missing lib params for domain --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 82b705a5c7..2cf57b95a3 100644 --- a/composer.lock +++ b/composer.lock @@ -5184,16 +5184,16 @@ }, { "name": "utopia-php/validators", - "version": "0.2.1", + "version": "0.2.2", "source": { "type": "git", "url": "https://github.com/utopia-php/validators.git", - "reference": "6cce9f73aa79f30de54aa3ff117090af570027cb" + "reference": "5d7d494e64457cd4eb67fdcfd9481f2c89796aa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/validators/zipball/6cce9f73aa79f30de54aa3ff117090af570027cb", - "reference": "6cce9f73aa79f30de54aa3ff117090af570027cb", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/5d7d494e64457cd4eb67fdcfd9481f2c89796aa6", + "reference": "5d7d494e64457cd4eb67fdcfd9481f2c89796aa6", "shasum": "" }, "require": { @@ -5223,9 +5223,9 @@ ], "support": { "issues": "https://github.com/utopia-php/validators/issues", - "source": "https://github.com/utopia-php/validators/tree/0.2.1" + "source": "https://github.com/utopia-php/validators/tree/0.2.2" }, - "time": "2026-04-27T16:05:19+00:00" + "time": "2026-04-27T16:30:24+00:00" }, { "name": "utopia-php/vcs", From d25707346fd7213a5ed0421656da522fe6a656e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 09:47:27 +0200 Subject: [PATCH 46/51] Add console oauth endpoint --- app/init/models.php | 6 ++ .../console/list-oauth2-providers.md | 1 + .../Console/Http/OAuth2Providers/XList.php | 80 ++++++++++++++++ .../Modules/Console/Services/Http.php | 2 + .../Http/Project/OAuth2/Amazon/Update.php | 20 ++++ .../Http/Project/OAuth2/Apple/Update.php | 53 +++++++++++ .../Http/Project/OAuth2/Auth0/Update.php | 32 +++++++ .../Http/Project/OAuth2/Authentik/Update.php | 32 +++++++ .../Http/Project/OAuth2/Autodesk/Update.php | 20 ++++ .../Project/Http/Project/OAuth2/Base.php | 91 +++++++++++++++++++ .../Http/Project/OAuth2/Bitbucket/Update.php | 20 ++++ .../Http/Project/OAuth2/Bitly/Update.php | 20 ++++ .../Http/Project/OAuth2/Box/Update.php | 20 ++++ .../Project/OAuth2/Dailymotion/Update.php | 20 ++++ .../Http/Project/OAuth2/Discord/Update.php | 20 ++++ .../Http/Project/OAuth2/Disqus/Update.php | 20 ++++ .../Http/Project/OAuth2/Dropbox/Update.php | 20 ++++ .../Http/Project/OAuth2/Etsy/Update.php | 20 ++++ .../Http/Project/OAuth2/Facebook/Update.php | 20 ++++ .../Http/Project/OAuth2/Figma/Update.php | 20 ++++ .../Http/Project/OAuth2/GitHub/Update.php | 25 +++++ .../Http/Project/OAuth2/Gitlab/Update.php | 32 +++++++ .../Http/Project/OAuth2/Google/Update.php | 20 ++++ .../Http/Project/OAuth2/Kick/Update.php | 20 ++++ .../Http/Project/OAuth2/Linkedin/Update.php | 20 ++++ .../Http/Project/OAuth2/Microsoft/Update.php | 32 +++++++ .../Http/Project/OAuth2/Notion/Update.php | 20 ++++ .../Http/Project/OAuth2/Oidc/Update.php | 50 ++++++++++ .../Http/Project/OAuth2/Okta/Update.php | 38 ++++++++ .../Http/Project/OAuth2/Paypal/Update.php | 20 ++++ .../Http/Project/OAuth2/Podio/Update.php | 20 ++++ .../Http/Project/OAuth2/Salesforce/Update.php | 20 ++++ .../Http/Project/OAuth2/Slack/Update.php | 20 ++++ .../Http/Project/OAuth2/Spotify/Update.php | 20 ++++ .../Http/Project/OAuth2/Stripe/Update.php | 20 ++++ .../Http/Project/OAuth2/Tradeshift/Update.php | 20 ++++ .../Http/Project/OAuth2/Twitch/Update.php | 20 ++++ .../Http/Project/OAuth2/WordPress/Update.php | 20 ++++ .../Project/Http/Project/OAuth2/X/Update.php | 20 ++++ .../Http/Project/OAuth2/Yahoo/Update.php | 20 ++++ .../Http/Project/OAuth2/Yandex/Update.php | 20 ++++ .../Http/Project/OAuth2/Zoho/Update.php | 20 ++++ .../Http/Project/OAuth2/Zoom/Update.php | 20 ++++ src/Appwrite/Utopia/Response.php | 3 + .../Response/Model/ConsoleOAuth2Provider.php | 37 ++++++++ .../Model/ConsoleOAuth2ProviderList.php | 37 ++++++++ .../Model/ConsoleOAuth2ProviderParameter.php | 49 ++++++++++ .../Utopia/Response/Model/OAuth2Linkedin.php | 2 +- .../Console/ConsoleConsoleClientTest.php | 87 ++++++++++++++++++ .../Console/ConsoleCustomServerTest.php | 19 ++++ 50 files changed, 1307 insertions(+), 1 deletion(-) create mode 100644 docs/references/console/list-oauth2-providers.md create mode 100644 src/Appwrite/Platform/Modules/Console/Http/OAuth2Providers/XList.php create mode 100644 src/Appwrite/Utopia/Response/Model/ConsoleOAuth2Provider.php create mode 100644 src/Appwrite/Utopia/Response/Model/ConsoleOAuth2ProviderList.php create mode 100644 src/Appwrite/Utopia/Response/Model/ConsoleOAuth2ProviderParameter.php diff --git a/app/init/models.php b/app/init/models.php index 1f92c77cec..39bc90e23c 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -56,6 +56,9 @@ use Appwrite\Utopia\Response\Model\ColumnString; use Appwrite\Utopia\Response\Model\ColumnText; use Appwrite\Utopia\Response\Model\ColumnURL; use Appwrite\Utopia\Response\Model\ColumnVarchar; +use Appwrite\Utopia\Response\Model\ConsoleOAuth2Provider; +use Appwrite\Utopia\Response\Model\ConsoleOAuth2ProviderList; +use Appwrite\Utopia\Response\Model\ConsoleOAuth2ProviderParameter; use Appwrite\Utopia\Response\Model\ConsoleVariables; use Appwrite\Utopia\Response\Model\Continent; use Appwrite\Utopia\Response\Model\Country; @@ -476,6 +479,9 @@ Response::setModel(new Rule()); Response::setModel(new Schedule()); Response::setModel(new TemplateEmail()); Response::setModel(new ConsoleVariables()); +Response::setModel(new ConsoleOAuth2ProviderParameter()); +Response::setModel(new ConsoleOAuth2Provider()); +Response::setModel(new ConsoleOAuth2ProviderList()); Response::setModel(new MFAChallenge()); Response::setModel(new MFARecoveryCodes()); Response::setModel(new MFAType()); diff --git a/docs/references/console/list-oauth2-providers.md b/docs/references/console/list-oauth2-providers.md new file mode 100644 index 0000000000..d813296031 --- /dev/null +++ b/docs/references/console/list-oauth2-providers.md @@ -0,0 +1 @@ +List all OAuth2 providers supported by the Appwrite server, along with the parameters required to configure each provider. The response excludes mock providers but includes sandbox providers. diff --git a/src/Appwrite/Platform/Modules/Console/Http/OAuth2Providers/XList.php b/src/Appwrite/Platform/Modules/Console/Http/OAuth2Providers/XList.php new file mode 100644 index 0000000000..574f7a5f6a --- /dev/null +++ b/src/Appwrite/Platform/Modules/Console/Http/OAuth2Providers/XList.php @@ -0,0 +1,80 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) + ->setHttpPath('/v1/console/oauth2-providers') + ->desc('List OAuth2 providers') + ->groups(['api']) + ->label('scope', 'public') + ->label('sdk', new Method( + namespace: 'console', + group: 'console', + name: 'listOAuth2Providers', + description: '/docs/references/console/list-oauth2-providers.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: Response::MODEL_CONSOLE_OAUTH2_PROVIDER_LIST, + ) + ], + contentType: ContentType::JSON + )) + ->inject('response') + ->callback($this->action(...)); + } + + public function action(Response $response): void + { + $providersConfig = Config::getParam('oAuthProviders', []); + $actions = OAuth2Base::getProviderActions(); + + $providers = []; + foreach ($actions as $providerId => $updateClass) { + $config = $providersConfig[$providerId] ?? null; + if ($config === null) { + continue; + } + if (!($config['enabled'] ?? false)) { + continue; + } + if ($config['mock'] ?? false) { + continue; + } + + $providers[] = new Document([ + '$id' => $providerId, + 'parameters' => $updateClass::getParameters(), + ]); + } + + $response->dynamic(new Document([ + 'total' => \count($providers), + 'oAuth2Providers' => $providers, + ]), Response::MODEL_CONSOLE_OAUTH2_PROVIDER_LIST); + } +} diff --git a/src/Appwrite/Platform/Modules/Console/Services/Http.php b/src/Appwrite/Platform/Modules/Console/Services/Http.php index f3ca6218f2..77029af0f9 100644 --- a/src/Appwrite/Platform/Modules/Console/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Console/Services/Http.php @@ -5,6 +5,7 @@ namespace Appwrite\Platform\Modules\Console\Services; use Appwrite\Platform\Modules\Console\Http\Assistant\Create as CreateAssistantQuery; use Appwrite\Platform\Modules\Console\Http\Init\API; use Appwrite\Platform\Modules\Console\Http\Init\Web; +use Appwrite\Platform\Modules\Console\Http\OAuth2Providers\XList as ListOAuth2Providers; use Appwrite\Platform\Modules\Console\Http\Redirects\Auth\Get as RedirectAuth; use Appwrite\Platform\Modules\Console\Http\Redirects\Card\Get as RedirectCard; use Appwrite\Platform\Modules\Console\Http\Redirects\Invite\Get as RedirectInvite; @@ -28,6 +29,7 @@ class Http extends Service $this->addAction(Web::getName(), new Web()); $this->addAction(GetVariables::getName(), new GetVariables()); + $this->addAction(ListOAuth2Providers::getName(), new ListOAuth2Providers()); $this->addAction(CreateAssistantQuery::getName(), new CreateAssistantQuery()); $this->addAction(GetResourceAvailability::getName(), new GetResourceAvailability()); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php index 1542f3b3bc..0fa0c187c9 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client Secret\' of Amazon OAuth2 app. For example: 79ffe4000000000000000000000000000000000000000000000000000002de55'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return 'amzn1.application-oa2-client.87400c00000000000000000000063d5b2'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return '79ffe4000000000000000000000000000000000000000000000000000002de55'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php index c2b0885f5f..6e8a75990a 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php @@ -61,6 +61,59 @@ class Update extends Base return ''; } + public static function getClientIdName(): string + { + return 'Service ID'; + } + + public static function getClientIdExample(): string + { + return 'ip.appwrite.app.web'; + } + + public static function getClientSecretName(): string + { + // Apple does not use a single clientSecret param. Returning an empty + // string causes the default getParameters() to skip it; the override + // below adds the three real fields (keyId, teamId, p8File). + return ''; + } + + public static function getClientSecretExample(): string + { + return ''; + } + + public static function getParameters(): array + { + return [ + [ + '$id' => static::getClientIdParamName(), + 'name' => static::getClientIdName(), + 'example' => static::getClientIdExample(), + 'hint' => '', + ], + [ + '$id' => 'keyId', + 'name' => 'Key ID', + 'example' => 'P4000000N8', + 'hint' => '', + ], + [ + '$id' => 'teamId', + 'name' => 'Team ID', + 'example' => 'D4000000R6', + 'hint' => '', + ], + [ + '$id' => 'p8File', + 'name' => 'P8 File', + 'example' => '-----BEGIN PRIVATE KEY-----MIGTAg...jy2Xbna-----END PRIVATE KEY-----', + 'hint' => '', + ], + ]; + } + public function __construct() { $providerId = static::getProviderId(); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php index 9c94864a50..38ac453ece 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php @@ -54,6 +54,38 @@ class Update extends Base return '\'Client Secret\' of Auth0 OAuth2 app. For example: zXz0000-00000000000000000000000000000-00000000000000000000PJafnF'; } + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return 'OaOkIA000000000000000000005KLSYq'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'zXz0000-00000000000000000000000000000-00000000000000000000PJafnF'; + } + + public static function getParameters(): array + { + return \array_merge(parent::getParameters(), [ + [ + '$id' => 'endpoint', + 'name' => 'Domain', + 'example' => 'example.us.auth0.com', + 'hint' => '', + ], + ]); + } + public function __construct() { $providerId = static::getProviderId(); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php index c4e27899a8..97f78f8013 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php @@ -54,6 +54,38 @@ class Update extends Base return '\'Client Secret\' of Authentik OAuth2 app. For example: ntQadq000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000Hp5WK'; } + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return 'dTKOPa0000000000000000000000000000e7G8hv'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'ntQadq000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000Hp5WK'; + } + + public static function getParameters(): array + { + return \array_merge(parent::getParameters(), [ + [ + '$id' => 'endpoint', + 'name' => 'Domain', + 'example' => 'example.authentik.com', + 'hint' => '', + ], + ]); + } + public function __construct() { $providerId = static::getProviderId(); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php index 6331f23080..b0595cd524 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'client secret\' of Autodesk OAuth2 app. For example: 7I000000000000MW'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return '5zw90v00000000000000000000kVYXN7'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return '7I000000000000MW'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php index 6591270ded..25acb75ee9 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php @@ -64,6 +64,97 @@ abstract class Base extends Action */ abstract public static function getClientSecretDescription(): string; + /** + * Verbose, user-facing name of the clientId param. Includes alternate + * names when the provider exposes more than one (e.g. "Client ID or App + * ID", "Application ID (also known as Client ID)"). + * + * @return string + */ + abstract public static function getClientIdName(): string; + + /** + * Example value of the clientId param. Used to build the public OAuth2 + * providers metadata response. + * + * @return string + */ + abstract public static function getClientIdExample(): string; + + /** + * Optional hint for the clientId param. Typically used to call out a + * common wrong value (e.g. "Example of wrong value: 370006"). Defaults + * to an empty string. + */ + public static function getClientIdHint(): string + { + return ''; + } + + /** + * Verbose, user-facing name of the clientSecret param. Returns an empty + * string for providers that don't have a single clientSecret param + * (e.g. Apple uses keyId/teamId/p8File instead). + * + * @return string + */ + abstract public static function getClientSecretName(): string; + + /** + * Example value of the clientSecret param. Returns an empty string for + * providers without a clientSecret param. + * + * @return string + */ + abstract public static function getClientSecretExample(): string; + + /** + * Optional hint for the clientSecret param. Defaults to an empty string. + */ + public static function getClientSecretHint(): string + { + return ''; + } + + /** + * Public-facing parameter metadata for this provider. Used by the public + * console OAuth2 providers endpoint to describe the form fields a project + * owner must fill in to configure the provider. + * + * Default shape: clientId + clientSecret. Providers that take additional + * fields (Apple, Auth0, Authentik, Gitlab, Microsoft, Oidc, Okta) + * override this method to add or replace entries. Each parameter is an + * associative array with keys `$id`, `name`, `example`, `hint`. + * + * @return array> + */ + public static function getParameters(): array + { + $parameters = []; + + $clientIdName = static::getClientIdName(); + if ($clientIdName !== '') { + $parameters[] = [ + '$id' => static::getClientIdParamName(), + 'name' => $clientIdName, + 'example' => static::getClientIdExample(), + 'hint' => static::getClientIdHint(), + ]; + } + + $clientSecretName = static::getClientSecretName(); + if ($clientSecretName !== '') { + $parameters[] = [ + '$id' => static::getClientSecretParamName(), + 'name' => $clientSecretName, + 'example' => static::getClientSecretExample(), + 'hint' => static::getClientSecretHint(), + ]; + } + + return $parameters; + } + /** * Public-facing name of the clientId param. Some providers use a different * terminology (e.g. Dropbox calls it "App key"), so the param name and the diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php index cbb48445b5..4321a56f30 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php @@ -52,4 +52,24 @@ class Update extends Base { return '\'Secret\' of Bitbucket OAuth2 app. For example: NMfLZJ00000000000000000000TLQdDx'; } + + public static function getClientIdName(): string + { + return 'Key'; + } + + public static function getClientIdExample(): string + { + return 'Knt70000000000ByRc'; + } + + public static function getClientSecretName(): string + { + return 'Secret'; + } + + public static function getClientSecretExample(): string + { + return 'NMfLZJ00000000000000000000TLQdDx'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php index d8964610e6..ebcb6837d2 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client Secret\' of Bitly OAuth2 app. For example: a13e250000000000000000000000000000d73095'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return 'd95151000000000000000000000000000067af9b'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'a13e250000000000000000000000000000d73095'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php index 8cb9df835a..ebc847f553 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client Secret\' of Box OAuth2 app. For example: OKM1f100000000000000000000eshEif'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return 'deglcs00000000000000000000x2og6y'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'OKM1f100000000000000000000eshEif'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php index d2f38309b4..d29d92c0f6 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php @@ -52,4 +52,24 @@ class Update extends Base { return '\'API secret\' of Dailymotion OAuth2 app. For example: a399a90000000000000000000000000000d90639'; } + + public static function getClientIdName(): string + { + return 'API Key'; + } + + public static function getClientIdExample(): string + { + return '07a9000000000000067f'; + } + + public static function getClientSecretName(): string + { + return 'API Secret'; + } + + public static function getClientSecretExample(): string + { + return 'a399a90000000000000000000000000000d90639'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php index 5efc193019..2d4dd805f9 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client Secret\' of Discord OAuth2 app. For example: YmPXnM000000000000000000002zFg5D'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return '950722000000343754'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'YmPXnM000000000000000000002zFg5D'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php index e77cd9b152..74cc714e35 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php @@ -52,4 +52,24 @@ class Update extends Base { return '\'Secret Key\', also known as \'API Secret\', of Disqus OAuth2 app. For example: W7Bykj00000000000000000000000000000000000000000000000000003o43w9'; } + + public static function getClientIdName(): string + { + return 'Public Key, also known as API Key'; + } + + public static function getClientIdExample(): string + { + return 'cgegH70000000000000000000000000000000000000000000000000000Hr1nYX'; + } + + public static function getClientSecretName(): string + { + return 'Secret Key, also known as API Secret'; + } + + public static function getClientSecretExample(): string + { + return 'W7Bykj00000000000000000000000000000000000000000000000000003o43w9'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php index 385b7719df..b6dc21e790 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php @@ -52,4 +52,24 @@ class Update extends Base { return '\'App secret\' of Dropbox OAuth2 app. For example: g200000000000vw'; } + + public static function getClientIdName(): string + { + return 'App Key'; + } + + public static function getClientIdExample(): string + { + return 'jl000000000009t'; + } + + public static function getClientSecretName(): string + { + return 'App Secret'; + } + + public static function getClientSecretExample(): string + { + return 'g200000000000vw'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php index 291daec414..8993d8f0ef 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php @@ -52,4 +52,24 @@ class Update extends Base { return '\'Shared Secret\' of Etsy OAuth2 app. For example: tp000000ru'; } + + public static function getClientIdName(): string + { + return 'Keystring'; + } + + public static function getClientIdExample(): string + { + return 'nsgzxh0000000000008j85a2'; + } + + public static function getClientSecretName(): string + { + return 'Shared Secret'; + } + + public static function getClientSecretExample(): string + { + return 'tp000000ru'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php index a3f97334a3..af3a42c94b 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php @@ -52,4 +52,24 @@ class Update extends Base { return '\'App secret\' of Facebook OAuth2 app. For example: 2d0b2800000000000000000000d38af4'; } + + public static function getClientIdName(): string + { + return 'App ID'; + } + + public static function getClientIdExample(): string + { + return '260600000007694'; + } + + public static function getClientSecretName(): string + { + return 'App Secret'; + } + + public static function getClientSecretExample(): string + { + return '2d0b2800000000000000000000d38af4'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php index b005bf17c9..06fd3ebc5a 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client Secret\' of Figma OAuth2 app. For example: yEpOYn0000000000000000004iIsU5'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return 'byay5H0000000000VtiI40'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'yEpOYn0000000000000000004iIsU5'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php index 3d4f77f117..6858fcf996 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php @@ -42,4 +42,29 @@ class Update extends Base { return '\'Client secret\' of GitHub OAuth2 app, or GitHub generic app. For example: 5e07c00000000000000000000000000000198bcc'; } + + public static function getClientIdName(): string + { + return 'Client ID or App ID'; + } + + public static function getClientIdExample(): string + { + return 'e4d87900000000540733'; + } + + public static function getClientIdHint(): string + { + return 'Example of wrong value: 370006'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return '5e07c00000000000000000000000000000198bcc'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php index 70c538454f..474780312b 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php @@ -65,6 +65,38 @@ class Update extends Base return '\'Secret\' of GitLab OAuth2 app. For example: gloas-838cfa0000000000000000000000000000000000000000000000000000ecbb38'; } + public static function getClientIdName(): string + { + return 'Application ID'; + } + + public static function getClientIdExample(): string + { + return 'd41ffe0000000000000000000000000000000000000000000000000000d5e252'; + } + + public static function getClientSecretName(): string + { + return 'Secret'; + } + + public static function getClientSecretExample(): string + { + return 'gloas-838cfa0000000000000000000000000000000000000000000000000000ecbb38'; + } + + public static function getParameters(): array + { + return \array_merge(parent::getParameters(), [ + [ + '$id' => 'endpoint', + 'name' => 'Endpoint', + 'example' => 'https://gitlab.com', + 'hint' => '', + ], + ]); + } + public function __construct() { $providerId = static::getProviderId(); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php index 796b6dae20..76bff1f34d 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client secret\' of Google OAuth2 app. For example: GOCSPX-2k8gsR0000000000000000VNahJj'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return '120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'GOCSPX-2k8gsR0000000000000000VNahJj'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Kick/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Kick/Update.php index b5c126a08c..f054c81ecf 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Kick/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Kick/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client Secret\' of Kick OAuth2 app. For example: 34ac5600000000000000000000000000000000000000000000000000e830c8b'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return '01KQ7C00000000000001MFHS32'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return '34ac5600000000000000000000000000000000000000000000000000e830c8b'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php index f23908279e..72f9fc1825 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php @@ -47,4 +47,24 @@ class Update extends Base { return '\'Primary Client Secret\' or \'Secondary Client Secret\', of LinkedIn OAuth2 app. For example: WPL_AP1.2Bf0000000000000./HtlYw=='; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return '770000000000dv'; + } + + public static function getClientSecretName(): string + { + return 'Primary Client Secret or Secondary Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'WPL_AP1.2Bf0000000000000./HtlYw=='; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php index 5f72b65dd8..a276ca60bb 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php @@ -64,6 +64,38 @@ class Update extends Base return '\'Application Secret\' (also known as Client Secret) of Microsoft Entra ID app. For example: A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u'; } + public static function getClientIdName(): string + { + return 'Application ID (also known as Client ID)'; + } + + public static function getClientIdExample(): string + { + return '00001111-aaaa-2222-bbbb-3333cccc4444'; + } + + public static function getClientSecretName(): string + { + return 'Application Secret (also known as Client Secret)'; + } + + public static function getClientSecretExample(): string + { + return 'A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u'; + } + + public static function getParameters(): array + { + return \array_merge(parent::getParameters(), [ + [ + '$id' => 'tenant', + 'name' => 'Tenant', + 'example' => 'common', + 'hint' => '', + ], + ]); + } + public function __construct() { $providerId = static::getProviderId(); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php index 56451166a4..b85c7158a7 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php @@ -52,4 +52,24 @@ class Update extends Base { return '\'OAuth Client Secret\' of Notion OAuth2 app. For example: secret_dLUr4b000000000000000000000000000000lFHAa9'; } + + public static function getClientIdName(): string + { + return 'OAuth Client ID'; + } + + public static function getClientIdExample(): string + { + return '341d8700-0000-0000-0000-000000446ee3'; + } + + public static function getClientSecretName(): string + { + return 'OAuth Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'secret_dLUr4b000000000000000000000000000000lFHAa9'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php index c000b456ec..55a14307cd 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php @@ -56,6 +56,56 @@ class Update extends Base return '\'Client Secret\' of OpenID Connect OAuth2 app. For example: Ah68ed000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003qpcHV'; } + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return 'qibI2x0000000000000000000000000006L2YFoG'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'Ah68ed000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003qpcHV'; + } + + public static function getParameters(): array + { + return \array_merge(parent::getParameters(), [ + [ + '$id' => 'wellKnownURL', + 'name' => 'Well-known URL', + 'example' => 'https://myoauth.com/.well-known/openid-configuration', + 'hint' => '', + ], + [ + '$id' => 'authorizationURL', + 'name' => 'Authorization URL', + 'example' => 'https://myoauth.com/oauth2/authorize', + 'hint' => '', + ], + [ + '$id' => 'tokenUrl', + 'name' => 'Token URL', + 'example' => 'https://myoauth.com/oauth2/token', + 'hint' => '', + ], + [ + '$id' => 'userInfoUrl', + 'name' => 'User Info URL', + 'example' => 'https://myoauth.com/oauth2/userinfo', + 'hint' => '', + ], + ]); + } + public function __construct() { $providerId = static::getProviderId(); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php index 504c0636af..eb135798c5 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php @@ -56,6 +56,44 @@ class Update extends Base return '\'Client Secret\' of Okta OAuth2 app. For example: Kiq0000000000000000000000000000000000000-00000000000H2L5-3SJ-vRV'; } + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return '0oa00000000000000698'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'Kiq0000000000000000000000000000000000000-00000000000H2L5-3SJ-vRV'; + } + + public static function getParameters(): array + { + return \array_merge(parent::getParameters(), [ + [ + '$id' => 'domain', + 'name' => 'Domain', + 'example' => 'trial-6400025.okta.com', + 'hint' => 'Example of wrong value: trial-6400025-admin.okta.com, or https://trial-6400025.okta.com/', + ], + [ + '$id' => 'authorizationServerId', + 'name' => 'Authorization Server ID', + 'example' => 'aus000000000000000h7z', + 'hint' => '', + ], + ]); + } + public function __construct() { $providerId = static::getProviderId(); diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php index 36b50475da..0ed9596725 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php @@ -47,4 +47,24 @@ class Update extends Base { return '\'Secret key 1\', or \'Secret key 2\', of ' . static::getProviderLabel() . ' OAuth2 app. For example: EH8KCXtew--000000000000000000000000000000000000000_C-1_5UP_000000000000000CB7KDp'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return 'AdhIEG7-000000000000-0000000000000000000000000000000-0000000000000000000000-2pyB'; + } + + public static function getClientSecretName(): string + { + return 'Secret Key 1 or Secret Key 2'; + } + + public static function getClientSecretExample(): string + { + return 'EH8KCXtew--000000000000000000000000000000000000000_C-1_5UP_000000000000000CB7KDp'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php index 47efa8b32b..72f7eb8f2c 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client Secret\' of Podio OAuth2 app. For example: Rn247T0000000000000000000000000000000000000000000000000000W2zWTN'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return 'appwrite-o0000000st-app'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'Rn247T0000000000000000000000000000000000000000000000000000W2zWTN'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php index 8721114327..1802932ce4 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php @@ -52,4 +52,24 @@ class Update extends Base { return '\'Consumer secret\' of Salesforce OAuth2 app. For example: 3w000000000000e2'; } + + public static function getClientIdName(): string + { + return 'Consumer Key'; + } + + public static function getClientIdExample(): string + { + return '3MVG9I0000000000000000000000000000000000000000000000000000000000000000000000000C5Aejq'; + } + + public static function getClientSecretName(): string + { + return 'Consumer Secret'; + } + + public static function getClientSecretExample(): string + { + return '3w000000000000e2'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php index 612bb26968..561563a37c 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client Secret\' of Slack OAuth2 app. For example: 81656000000000000000000000f3d2fd'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return '23000000089.15000000000023'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return '81656000000000000000000000f3d2fd'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php index d28bfac8a2..1134fd194a 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client secret\' of Spotify OAuth2 app. For example: db068a000000000000000000008b5b9f'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return '6ec271000000000000000000009beace'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'db068a000000000000000000008b5b9f'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php index 605804fa96..4702ef271d 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php @@ -47,4 +47,24 @@ class Update extends Base { return '\'API Secret key\' of Stripe OAuth2 app. For example: sk_51SfOd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000QGWYfp'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return 'ca_UKibXX0000000000000000000006byvR'; + } + + public static function getClientSecretName(): string + { + return 'API Secret Key'; + } + + public static function getClientSecretExample(): string + { + return 'sk_51SfOd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000QGWYfp'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Tradeshift/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Tradeshift/Update.php index bff866cde6..3d0e05b886 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Tradeshift/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Tradeshift/Update.php @@ -52,4 +52,24 @@ class Update extends Base { return '\'Oauth2 Client secret\' of ' . static::getProviderLabel() . ' OAuth2 app. For example: 7cb52700-0000-0000-0000-000000ca5b83'; } + + public static function getClientIdName(): string + { + return 'OAuth2 Client ID'; + } + + public static function getClientIdExample(): string + { + return 'appwrite-tes00000.0000000000est-app'; + } + + public static function getClientSecretName(): string + { + return 'OAuth2 Client Secret'; + } + + public static function getClientSecretExample(): string + { + return '7cb52700-0000-0000-0000-000000ca5b83'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Twitch/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Twitch/Update.php index 09dfadb697..7377ba421d 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Twitch/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Twitch/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client Secret\' of Twitch OAuth2 app. For example: pmapue000000000000000000zylw3v'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return 'vvi0in000000000000000000ikmt9p'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'pmapue000000000000000000zylw3v'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/WordPress/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/WordPress/Update.php index 706638c6ce..b8b49f6970 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/WordPress/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/WordPress/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client Secret\' of WordPress OAuth2 app. For example: PlBfJS0000000000000000000000000000000000000000000000000000EdUZJk'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return '130005'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'PlBfJS0000000000000000000000000000000000000000000000000000EdUZJk'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/X/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/X/Update.php index b38eab0ab0..83b4048ba5 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/X/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/X/Update.php @@ -52,4 +52,24 @@ class Update extends Base { return '\'Secret Key\' of X OAuth2 app. For example: tkEPkp00000000000000000000000000000000000000FTxbI9'; } + + public static function getClientIdName(): string + { + return 'Customer Key'; + } + + public static function getClientIdExample(): string + { + return 'slzZV0000000000000NFLaWT'; + } + + public static function getClientSecretName(): string + { + return 'Secret Key'; + } + + public static function getClientSecretExample(): string + { + return 'tkEPkp00000000000000000000000000000000000000FTxbI9'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yahoo/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yahoo/Update.php index 512c8b1e6d..62c19851ab 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yahoo/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yahoo/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client Secret\', also known as \'Customer Secret\', of Yahoo OAuth2 app. For example: cf978f0000000000000000000000000000c5e2e9'; } + + public static function getClientIdName(): string + { + return 'Client ID, also known as Customer Key'; + } + + public static function getClientIdExample(): string + { + return 'dj0yJm000000000000000000000000000000000000000000000000000000000000000000000000000000000000Z4PWRm'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret, also known as Customer Secret'; + } + + public static function getClientSecretExample(): string + { + return 'cf978f0000000000000000000000000000c5e2e9'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yandex/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yandex/Update.php index 31f8cd771e..8e5e5839a8 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yandex/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yandex/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client secret\' of Yandex OAuth2 app. For example: bbf98500000000000000000000c75a63'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return '6a8a6a0000000000000000000091483c'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'bbf98500000000000000000000c75a63'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoho/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoho/Update.php index a663667af7..75fa3692bd 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoho/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoho/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client Secret\' of Zoho OAuth2 app. For example: fb5cac000000000000000000000000000000a68f6e'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return '1000.83C178000000000000000000RPNX0B'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'fb5cac000000000000000000000000000000a68f6e'; + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoom/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoom/Update.php index 4edea07891..b0e999b256 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoom/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoom/Update.php @@ -42,4 +42,24 @@ class Update extends Base { return '\'Client Secret\' of Zoom OAuth2 app. For example: GAWsG4000000000000000000007U01ON'; } + + public static function getClientIdName(): string + { + return 'Client ID'; + } + + public static function getClientIdExample(): string + { + return 'QMAC00000000000000w0AQ'; + } + + public static function getClientSecretName(): string + { + return 'Client Secret'; + } + + public static function getClientSecretExample(): string + { + return 'GAWsG4000000000000000000007U01ON'; + } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 4dbcf135af..7670b027e9 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -329,6 +329,9 @@ class Response extends SwooleResponse // Console public const MODEL_CONSOLE_VARIABLES = 'consoleVariables'; + public const MODEL_CONSOLE_OAUTH2_PROVIDER_PARAMETER = 'consoleOAuth2ProviderParameter'; + public const MODEL_CONSOLE_OAUTH2_PROVIDER = 'consoleOAuth2Provider'; + public const MODEL_CONSOLE_OAUTH2_PROVIDER_LIST = 'consoleOAuth2ProviderList'; // Deprecated public const MODEL_PERMISSIONS = 'permissions'; diff --git a/src/Appwrite/Utopia/Response/Model/ConsoleOAuth2Provider.php b/src/Appwrite/Utopia/Response/Model/ConsoleOAuth2Provider.php new file mode 100644 index 0000000000..05969a5e8c --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ConsoleOAuth2Provider.php @@ -0,0 +1,37 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'OAuth2 provider ID.', + 'default' => '', + 'example' => 'github', + ]) + ->addRule('parameters', [ + 'type' => Response::MODEL_CONSOLE_OAUTH2_PROVIDER_PARAMETER, + 'description' => 'List of parameters required to configure this OAuth2 provider.', + 'default' => [], + 'array' => true, + ]) + ; + } + + public function getName(): string + { + return 'Console OAuth2 Provider'; + } + + public function getType(): string + { + return Response::MODEL_CONSOLE_OAUTH2_PROVIDER; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/ConsoleOAuth2ProviderList.php b/src/Appwrite/Utopia/Response/Model/ConsoleOAuth2ProviderList.php new file mode 100644 index 0000000000..42d6936d42 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ConsoleOAuth2ProviderList.php @@ -0,0 +1,37 @@ +addRule('total', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total number of OAuth2 providers exposed by the server.', + 'default' => 0, + 'example' => 5, + ]) + ->addRule('oAuth2Providers', [ + 'type' => Response::MODEL_CONSOLE_OAUTH2_PROVIDER, + 'description' => 'List of OAuth2 providers, each with the parameters required to configure it.', + 'default' => [], + 'array' => true, + ]) + ; + } + + public function getName(): string + { + return 'Console OAuth2 Providers List'; + } + + public function getType(): string + { + return Response::MODEL_CONSOLE_OAUTH2_PROVIDER_LIST; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/ConsoleOAuth2ProviderParameter.php b/src/Appwrite/Utopia/Response/Model/ConsoleOAuth2ProviderParameter.php new file mode 100644 index 0000000000..a097718492 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/ConsoleOAuth2ProviderParameter.php @@ -0,0 +1,49 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Parameter ID. Maps to the request body field used by the project OAuth2 update endpoint (e.g. `clientId`, `appKey`, `tenant`).', + 'default' => '', + 'example' => 'clientId', + ]) + ->addRule('name', [ + 'type' => self::TYPE_STRING, + 'description' => 'Verbose, user-facing parameter name as shown in the provider\'s own dashboard. Includes alternate names when the provider exposes more than one.', + 'default' => '', + 'example' => 'Client ID or App ID', + ]) + ->addRule('example', [ + 'type' => self::TYPE_STRING, + 'description' => 'Example value for this parameter.', + 'default' => '', + 'example' => 'e4d87900000000540733', + ]) + ->addRule('hint', [ + 'type' => self::TYPE_STRING, + 'description' => 'Optional hint for this parameter, typically calling out a common wrong value. Empty string when no hint is set.', + 'default' => '', + 'example' => 'Example of wrong value: 370006', + ]) + ; + } + + public function getName(): string + { + return 'Console OAuth2 Provider Parameter'; + } + + public function getType(): string + { + return Response::MODEL_CONSOLE_OAUTH2_PROVIDER_PARAMETER; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Linkedin.php b/src/Appwrite/Utopia/Response/Model/OAuth2Linkedin.php index 99f8bfa8f7..012aa85735 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2Linkedin.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Linkedin.php @@ -22,7 +22,7 @@ class OAuth2Linkedin extends OAuth2Base public function getClientSecretExample(): string { - return 'WPL_AP1.2Bf0000000000000'; + return 'WPL_AP1.2Bf0000000000000./HtlYw=='; } public function getClientSecretFieldName(): string diff --git a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php index 373383e3ec..779ede8d9c 100644 --- a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php +++ b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php @@ -41,4 +41,91 @@ class ConsoleConsoleClientTest extends Scope $this->assertIsString($response['body']['_APP_DB_ADAPTER']); // When adding new keys, dont forget to update count a few lines above } + + public function testListOAuth2Providers(): void + { + $response = $this->client->call(Client::METHOD_GET, '/console/oauth2-providers', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertIsInt($response['body']['total']); + $this->assertIsArray($response['body']['oAuth2Providers']); + $this->assertGreaterThan(0, $response['body']['total']); + $this->assertEquals($response['body']['total'], \count($response['body']['oAuth2Providers'])); + + $providerIds = \array_column($response['body']['oAuth2Providers'], '$id'); + + // Well-known providers must be present + $this->assertContains('github', $providerIds); + $this->assertContains('google', $providerIds); + + // Mock providers must be excluded + $this->assertNotContains('mock', $providerIds); + $this->assertNotContains('mock-unverified', $providerIds); + + // Every provider has the expected shape + foreach ($response['body']['oAuth2Providers'] as $provider) { + $this->assertArrayHasKey('$id', $provider); + $this->assertIsString($provider['$id']); + $this->assertArrayHasKey('parameters', $provider); + $this->assertIsArray($provider['parameters']); + $this->assertGreaterThan(0, \count($provider['parameters'])); + + foreach ($provider['parameters'] as $parameter) { + $this->assertArrayHasKey('$id', $parameter); + $this->assertIsString($parameter['$id']); + $this->assertNotEmpty($parameter['$id']); + $this->assertArrayHasKey('name', $parameter); + $this->assertIsString($parameter['name']); + $this->assertNotEmpty($parameter['name']); + $this->assertArrayHasKey('example', $parameter); + $this->assertIsString($parameter['example']); + $this->assertArrayHasKey('hint', $parameter); + $this->assertIsString($parameter['hint']); + } + } + + // GitHub provider has the expected metadata for clientId, including the hint + $github = null; + foreach ($response['body']['oAuth2Providers'] as $provider) { + if ($provider['$id'] === 'github') { + $github = $provider; + break; + } + } + $this->assertNotNull($github); + $this->assertCount(2, $github['parameters']); + $clientId = $github['parameters'][0]; + $this->assertEquals('clientId', $clientId['$id']); + $this->assertEquals('Client ID or App ID', $clientId['name']); + $this->assertEquals('e4d87900000000540733', $clientId['example']); + $this->assertEquals('Example of wrong value: 370006', $clientId['hint']); + $clientSecret = $github['parameters'][1]; + $this->assertEquals('clientSecret', $clientSecret['$id']); + $this->assertEquals('Client Secret', $clientSecret['name']); + $this->assertNotEmpty($clientSecret['example']); + $this->assertEquals('', $clientSecret['hint']); + + // Multi-parameter provider (Apple) exposes its non-clientSecret fields + $apple = null; + foreach ($response['body']['oAuth2Providers'] as $provider) { + if ($provider['$id'] === 'apple') { + $apple = $provider; + break; + } + } + $this->assertNotNull($apple); + $appleParamIds = \array_column($apple['parameters'], '$id'); + $this->assertContains('serviceId', $appleParamIds); + $this->assertContains('keyId', $appleParamIds); + $this->assertContains('teamId', $appleParamIds); + $this->assertContains('p8File', $appleParamIds); + // Apple does not expose a single clientSecret param + $this->assertNotContains('clientSecret', $appleParamIds); + + // Sandbox providers (e.g. paypalSandbox) are included + $this->assertContains('paypalSandbox', $providerIds); + } } diff --git a/tests/e2e/Services/Console/ConsoleCustomServerTest.php b/tests/e2e/Services/Console/ConsoleCustomServerTest.php index 3748bbe546..d3c64ae039 100644 --- a/tests/e2e/Services/Console/ConsoleCustomServerTest.php +++ b/tests/e2e/Services/Console/ConsoleCustomServerTest.php @@ -24,4 +24,23 @@ class ConsoleCustomServerTest extends Scope $this->assertEquals(401, $response['headers']['status-code']); } + + public function testListOAuth2Providers(): void + { + // Public endpoint: must succeed without admin authentication. Drop the + // headers from getHeaders() and only pass project + content-type. + $response = $this->client->call(Client::METHOD_GET, '/console/oauth2-providers', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertIsInt($response['body']['total']); + $this->assertIsArray($response['body']['oAuth2Providers']); + $this->assertGreaterThan(0, $response['body']['total']); + + $providerIds = \array_column($response['body']['oAuth2Providers'], '$id'); + $this->assertContains('github', $providerIds); + $this->assertNotContains('mock', $providerIds); + } } From e2bb9a916114452972c50e650a4624f63794f3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 10:08:39 +0200 Subject: [PATCH 47/51] Simplify oauth endpoints --- .../Http/Project/OAuth2/Amazon/Update.php | 10 ---- .../Http/Project/OAuth2/Apple/Update.php | 12 ----- .../Http/Project/OAuth2/Auth0/Update.php | 10 ---- .../Http/Project/OAuth2/Authentik/Update.php | 10 ---- .../Http/Project/OAuth2/Autodesk/Update.php | 10 ---- .../Project/Http/Project/OAuth2/Base.php | 52 ++++++++++++++++--- .../Http/Project/OAuth2/Bitbucket/Update.php | 10 ---- .../Http/Project/OAuth2/Bitly/Update.php | 10 ---- .../Http/Project/OAuth2/Box/Update.php | 10 ---- .../Project/OAuth2/Dailymotion/Update.php | 10 ---- .../Http/Project/OAuth2/Discord/Update.php | 10 ---- .../Http/Project/OAuth2/Disqus/Update.php | 10 ---- .../Http/Project/OAuth2/Dropbox/Update.php | 10 ---- .../Http/Project/OAuth2/Etsy/Update.php | 10 ---- .../Http/Project/OAuth2/Facebook/Update.php | 10 ---- .../Http/Project/OAuth2/Figma/Update.php | 10 ---- .../Http/Project/OAuth2/GitHub/Update.php | 10 ---- .../Http/Project/OAuth2/Gitlab/Update.php | 10 ---- .../Http/Project/OAuth2/Google/Update.php | 10 ---- .../Http/Project/OAuth2/Kick/Update.php | 10 ---- .../Http/Project/OAuth2/Linkedin/Update.php | 10 ---- .../Http/Project/OAuth2/Microsoft/Update.php | 10 ---- .../Http/Project/OAuth2/Notion/Update.php | 10 ---- .../Http/Project/OAuth2/Oidc/Update.php | 10 ---- .../Http/Project/OAuth2/Okta/Update.php | 10 ---- .../Http/Project/OAuth2/Paypal/Update.php | 10 ---- .../Http/Project/OAuth2/Podio/Update.php | 10 ---- .../Http/Project/OAuth2/Salesforce/Update.php | 10 ---- .../Http/Project/OAuth2/Slack/Update.php | 10 ---- .../Http/Project/OAuth2/Spotify/Update.php | 10 ---- .../Http/Project/OAuth2/Stripe/Update.php | 10 ---- .../Http/Project/OAuth2/Tradeshift/Update.php | 10 ---- .../Http/Project/OAuth2/Twitch/Update.php | 10 ---- .../Http/Project/OAuth2/WordPress/Update.php | 10 ---- .../Project/Http/Project/OAuth2/X/Update.php | 10 ---- .../Http/Project/OAuth2/Yahoo/Update.php | 10 ---- .../Http/Project/OAuth2/Yandex/Update.php | 10 ---- .../Http/Project/OAuth2/Zoho/Update.php | 10 ---- .../Http/Project/OAuth2/Zoom/Update.php | 10 ---- 39 files changed, 44 insertions(+), 390 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php index 0fa0c187c9..7c68ff4032 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Amazon/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_AMAZON; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Amazon OAuth2 app. For example: amzn1.application-oa2-client.87400c00000000000000000000063d5b2'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of Amazon OAuth2 app. For example: 79ffe4000000000000000000000000000000000000000000000000000002de55'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php index 6e8a75990a..08fc7dbf6b 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Apple/Update.php @@ -49,18 +49,6 @@ class Update extends Base return 'serviceId'; } - public static function getClientIdDescription(): string - { - return '\'Service ID\' of Apple OAuth2 app. For example: ip.appwrite.app.web'; - } - - public static function getClientSecretDescription(): string - { - // Unused: this adapter replaces the single clientSecret param with - // keyId, teamId and p8File by overriding __construct() and handle(). - return ''; - } - public static function getClientIdName(): string { return 'Service ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php index 38ac453ece..aa5f39b213 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Auth0/Update.php @@ -44,16 +44,6 @@ class Update extends Base return Response::MODEL_OAUTH2_AUTH0; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Auth0 OAuth2 app. For example: OaOkIA000000000000000000005KLSYq'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of Auth0 OAuth2 app. For example: zXz0000-00000000000000000000000000000-00000000000000000000PJafnF'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php index 97f78f8013..d5d465c3d4 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Authentik/Update.php @@ -44,16 +44,6 @@ class Update extends Base return Response::MODEL_OAUTH2_AUTHENTIK; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Authentik OAuth2 app. For example: dTKOPa0000000000000000000000000000e7G8hv'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of Authentik OAuth2 app. For example: ntQadq000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000Hp5WK'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php index b0595cd524..dd4f4f6faa 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Autodesk/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_AUTODESK; } - public static function getClientIdDescription(): string - { - return '\'client ID\' of Autodesk OAuth2 app. For example: 5zw90v00000000000000000000kVYXN7'; - } - - public static function getClientSecretDescription(): string - { - return '\'client secret\' of Autodesk OAuth2 app. For example: 7I000000000000MW'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php index 25acb75ee9..b0f59e7c08 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php @@ -51,18 +51,54 @@ abstract class Base extends Action abstract public static function getResponseModel(): string; /** - * Description of the clientId param, including an example value. - * - * @return string + * Description of the clientId param, auto-built from + * {@see getClientIdName()}, {@see getClientIdExample()} and + * {@see getClientIdHint()}. Returns an empty string when the name is + * empty (e.g. providers like Apple that don't expose a single clientId + * description but still need to bypass this default). */ - abstract public static function getClientIdDescription(): string; + public static function getClientIdDescription(): string + { + return self::buildParamDescription( + static::getClientIdName(), + static::getClientIdExample(), + static::getClientIdHint() + ); + } /** - * Description of the clientSecret param, including an example value. - * - * @return string + * Description of the clientSecret param, auto-built from + * {@see getClientSecretName()}, {@see getClientSecretExample()} and + * {@see getClientSecretHint()}. Returns an empty string when the name + * is empty (e.g. Apple, which uses keyId/teamId/p8File instead). */ - abstract public static function getClientSecretDescription(): string; + public static function getClientSecretDescription(): string + { + return self::buildParamDescription( + static::getClientSecretName(), + static::getClientSecretExample(), + static::getClientSecretHint() + ); + } + + /** + * Format a parameter description as + * "'' of OAuth2 app. For example: [. ]". + * Returns an empty string when the name is empty. + */ + private static function buildParamDescription(string $name, string $example, string $hint): string + { + if ($name === '') { + return ''; + } + + $description = '\'' . $name . '\' of ' . static::getProviderLabel() . ' OAuth2 app. For example: ' . $example; + if ($hint !== '') { + $description .= '. ' . $hint; + } + + return $description; + } /** * Verbose, user-facing name of the clientId param. Includes alternate diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php index 4321a56f30..a477bfbefb 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitbucket/Update.php @@ -43,16 +43,6 @@ class Update extends Base return 'secret'; } - public static function getClientIdDescription(): string - { - return '\'Key\' of Bitbucket OAuth2 app. For example: Knt70000000000ByRc'; - } - - public static function getClientSecretDescription(): string - { - return '\'Secret\' of Bitbucket OAuth2 app. For example: NMfLZJ00000000000000000000TLQdDx'; - } - public static function getClientIdName(): string { return 'Key'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php index ebcb6837d2..731b71bbb3 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Bitly/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_BITLY; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Bitly OAuth2 app. For example: d95151000000000000000000000000000067af9b'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of Bitly OAuth2 app. For example: a13e250000000000000000000000000000d73095'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php index ebc847f553..113e5c8968 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Box/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_BOX; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Box OAuth2 app. For example: deglcs00000000000000000000x2og6y'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of Box OAuth2 app. For example: OKM1f100000000000000000000eshEif'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php index d29d92c0f6..5f7186a224 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dailymotion/Update.php @@ -43,16 +43,6 @@ class Update extends Base return 'apiSecret'; } - public static function getClientIdDescription(): string - { - return '\'API key\' of Dailymotion OAuth2 app. For example: 07a9000000000000067f'; - } - - public static function getClientSecretDescription(): string - { - return '\'API secret\' of Dailymotion OAuth2 app. For example: a399a90000000000000000000000000000d90639'; - } - public static function getClientIdName(): string { return 'API Key'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php index 2d4dd805f9..e4732912b9 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Discord/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_DISCORD; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Discord OAuth2 app. For example: 950722000000343754'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of Discord OAuth2 app. For example: YmPXnM000000000000000000002zFg5D'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php index 74cc714e35..e5f80c07d8 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Disqus/Update.php @@ -43,16 +43,6 @@ class Update extends Base return 'secretKey'; } - public static function getClientIdDescription(): string - { - return '\'Public key\', also known as \'API Key\', of Disqus OAuth2 app. For example: cgegH70000000000000000000000000000000000000000000000000000Hr1nYX'; - } - - public static function getClientSecretDescription(): string - { - return '\'Secret Key\', also known as \'API Secret\', of Disqus OAuth2 app. For example: W7Bykj00000000000000000000000000000000000000000000000000003o43w9'; - } - public static function getClientIdName(): string { return 'Public Key, also known as API Key'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php index b6dc21e790..861eca1cef 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Dropbox/Update.php @@ -43,16 +43,6 @@ class Update extends Base return 'appSecret'; } - public static function getClientIdDescription(): string - { - return '\'App key\' of Dropbox OAuth2 app. For example: jl000000000009t'; - } - - public static function getClientSecretDescription(): string - { - return '\'App secret\' of Dropbox OAuth2 app. For example: g200000000000vw'; - } - public static function getClientIdName(): string { return 'App Key'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php index 8993d8f0ef..0a9d0e9147 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Etsy/Update.php @@ -43,16 +43,6 @@ class Update extends Base return 'sharedSecret'; } - public static function getClientIdDescription(): string - { - return '\'Keystring\' of Etsy OAuth2 app. For example: nsgzxh0000000000008j85a2'; - } - - public static function getClientSecretDescription(): string - { - return '\'Shared Secret\' of Etsy OAuth2 app. For example: tp000000ru'; - } - public static function getClientIdName(): string { return 'Keystring'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php index af3a42c94b..766686273a 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Facebook/Update.php @@ -43,16 +43,6 @@ class Update extends Base return 'appSecret'; } - public static function getClientIdDescription(): string - { - return '\'App ID\' of Facebook OAuth2 app. For example: 260600000007694'; - } - - public static function getClientSecretDescription(): string - { - return '\'App secret\' of Facebook OAuth2 app. For example: 2d0b2800000000000000000000d38af4'; - } - public static function getClientIdName(): string { return 'App ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php index 06fd3ebc5a..a965da77a0 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Figma/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_FIGMA; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Figma OAuth2 app. For example: byay5H0000000000VtiI40'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of Figma OAuth2 app. For example: yEpOYn0000000000000000004iIsU5'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php index 6858fcf996..a82b3a3ea2 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_GITHUB; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of GitHub OAuth2 app, or \'App ID\' of GitHub generic app. For example: e4d87900000000540733. Example of wrong value: 370006'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client secret\' of GitHub OAuth2 app, or GitHub generic app. For example: 5e07c00000000000000000000000000000198bcc'; - } - public static function getClientIdName(): string { return 'Client ID or App ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php index 474780312b..804f6354ae 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Gitlab/Update.php @@ -55,16 +55,6 @@ class Update extends Base return 'secret'; } - public static function getClientIdDescription(): string - { - return '\'Application ID\' of GitLab OAuth2 app. For example: d41ffe0000000000000000000000000000000000000000000000000000d5e252'; - } - - public static function getClientSecretDescription(): string - { - return '\'Secret\' of GitLab OAuth2 app. For example: gloas-838cfa0000000000000000000000000000000000000000000000000000ecbb38'; - } - public static function getClientIdName(): string { return 'Application ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php index 76bff1f34d..9b985f4aed 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Google/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_GOOGLE; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Google OAuth2 app. For example: 120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client secret\' of Google OAuth2 app. For example: GOCSPX-2k8gsR0000000000000000VNahJj'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Kick/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Kick/Update.php index f054c81ecf..db4a20174f 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Kick/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Kick/Update.php @@ -33,16 +33,6 @@ class Update extends Base 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'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php index 72f9fc1825..d564f3aac5 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Linkedin/Update.php @@ -38,16 +38,6 @@ class Update extends Base return 'primaryClientSecret'; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of LinkedIn OAuth2 app. For example: 770000000000dv'; - } - - public static function getClientSecretDescription(): string - { - return '\'Primary Client Secret\' or \'Secondary Client Secret\', of LinkedIn OAuth2 app. For example: WPL_AP1.2Bf0000000000000./HtlYw=='; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php index a276ca60bb..fe4f4b263e 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php @@ -54,16 +54,6 @@ class Update extends Base return 'applicationSecret'; } - public static function getClientIdDescription(): string - { - return '\'Application ID\' (also known as Client ID) of Microsoft Entra ID app. For example: 00001111-aaaa-2222-bbbb-3333cccc4444'; - } - - public static function getClientSecretDescription(): string - { - return '\'Application Secret\' (also known as Client Secret) of Microsoft Entra ID app. For example: A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u'; - } - public static function getClientIdName(): string { return 'Application ID (also known as Client ID)'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php index b85c7158a7..4b048b0c0b 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Notion/Update.php @@ -43,16 +43,6 @@ class Update extends Base return 'oauthClientSecret'; } - public static function getClientIdDescription(): string - { - return '\'OAuth Client ID\' of Notion OAuth2 app. For example: 341d8700-0000-0000-0000-000000446ee3'; - } - - public static function getClientSecretDescription(): string - { - return '\'OAuth Client Secret\' of Notion OAuth2 app. For example: secret_dLUr4b000000000000000000000000000000lFHAa9'; - } - public static function getClientIdName(): string { return 'OAuth Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php index 55a14307cd..9598ff4c43 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Oidc/Update.php @@ -46,16 +46,6 @@ class Update extends Base return Response::MODEL_OAUTH2_OIDC; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of OpenID Connect OAuth2 app. For example: qibI2x0000000000000000000000000006L2YFoG'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of OpenID Connect OAuth2 app. For example: Ah68ed000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003qpcHV'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php index eb135798c5..0344b6a14a 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Okta/Update.php @@ -46,16 +46,6 @@ class Update extends Base return Response::MODEL_OAUTH2_OKTA; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Okta OAuth2 app. For example: 0oa00000000000000698'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of Okta OAuth2 app. For example: Kiq0000000000000000000000000000000000000-00000000000H2L5-3SJ-vRV'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php index 0ed9596725..87b4e1576b 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Paypal/Update.php @@ -38,16 +38,6 @@ class Update extends Base return 'secretKey'; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of ' . static::getProviderLabel() . ' OAuth2 app. For example: AdhIEG7-000000000000-0000000000000000000000000000000-0000000000000000000000-2pyB'; - } - - public static function getClientSecretDescription(): string - { - return '\'Secret key 1\', or \'Secret key 2\', of ' . static::getProviderLabel() . ' OAuth2 app. For example: EH8KCXtew--000000000000000000000000000000000000000_C-1_5UP_000000000000000CB7KDp'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php index 72f7eb8f2c..dc6647c2b1 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Podio/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_PODIO; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Podio OAuth2 app. For example: appwrite-o0000000st-app'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of Podio OAuth2 app. For example: Rn247T0000000000000000000000000000000000000000000000000000W2zWTN'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php index 1802932ce4..f04b9d75dd 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Salesforce/Update.php @@ -43,16 +43,6 @@ class Update extends Base return 'customerSecret'; } - public static function getClientIdDescription(): string - { - return '\'Consumer key\' of Salesforce OAuth2 app. For example: 3MVG9I0000000000000000000000000000000000000000000000000000000000000000000000000C5Aejq'; - } - - public static function getClientSecretDescription(): string - { - return '\'Consumer secret\' of Salesforce OAuth2 app. For example: 3w000000000000e2'; - } - public static function getClientIdName(): string { return 'Consumer Key'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php index 561563a37c..72ab62e1d5 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Slack/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_SLACK; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Slack OAuth2 app. For example: 23000000089.15000000000023'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of Slack OAuth2 app. For example: 81656000000000000000000000f3d2fd'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php index 1134fd194a..35128a8591 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Spotify/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_SPOTIFY; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Spotify OAuth2 app. For example: 6ec271000000000000000000009beace'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client secret\' of Spotify OAuth2 app. For example: db068a000000000000000000008b5b9f'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php index 4702ef271d..8c0bd5f14c 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Stripe/Update.php @@ -38,16 +38,6 @@ class Update extends Base return 'apiSecretKey'; } - public static function getClientIdDescription(): string - { - return '\'client ID\' of Stripe OAuth2 app. For example: ca_UKibXX0000000000000000000006byvR'; - } - - public static function getClientSecretDescription(): string - { - return '\'API Secret key\' of Stripe OAuth2 app. For example: sk_51SfOd000000000000000000000000000000000000000000000000000000000000000000000000000000000000000QGWYfp'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Tradeshift/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Tradeshift/Update.php index 3d0e05b886..6e93a22960 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Tradeshift/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Tradeshift/Update.php @@ -43,16 +43,6 @@ class Update extends Base return 'oauth2ClientSecret'; } - public static function getClientIdDescription(): string - { - return '\'Oauth2 Client ID\' of ' . static::getProviderLabel() . ' OAuth2 app. For example: appwrite-tes00000.0000000000est-app'; - } - - public static function getClientSecretDescription(): string - { - return '\'Oauth2 Client secret\' of ' . static::getProviderLabel() . ' OAuth2 app. For example: 7cb52700-0000-0000-0000-000000ca5b83'; - } - public static function getClientIdName(): string { return 'OAuth2 Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Twitch/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Twitch/Update.php index 7377ba421d..54a28f88cd 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Twitch/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Twitch/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_TWITCH; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Twitch OAuth2 app. For example: vvi0in000000000000000000ikmt9p'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of Twitch OAuth2 app. For example: pmapue000000000000000000zylw3v'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/WordPress/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/WordPress/Update.php index b8b49f6970..14ddf1552a 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/WordPress/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/WordPress/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_WORDPRESS; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of WordPress OAuth2 app. For example: 130005'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of WordPress OAuth2 app. For example: PlBfJS0000000000000000000000000000000000000000000000000000EdUZJk'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/X/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/X/Update.php index 83b4048ba5..3edc4709db 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/X/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/X/Update.php @@ -43,16 +43,6 @@ class Update extends Base return 'secretKey'; } - public static function getClientIdDescription(): string - { - return '\'Customer Key\' of X OAuth2 app. For example: slzZV0000000000000NFLaWT'; - } - - public static function getClientSecretDescription(): string - { - return '\'Secret Key\' of X OAuth2 app. For example: tkEPkp00000000000000000000000000000000000000FTxbI9'; - } - public static function getClientIdName(): string { return 'Customer Key'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yahoo/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yahoo/Update.php index 62c19851ab..45cf1f5a66 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yahoo/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yahoo/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_YAHOO; } - public static function getClientIdDescription(): string - { - return '\'Client ID\', also known as \'Customer Key\', of Yahoo OAuth2 app. For example: dj0yJm000000000000000000000000000000000000000000000000000000000000000000000000000000000000Z4PWRm'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\', also known as \'Customer Secret\', of Yahoo OAuth2 app. For example: cf978f0000000000000000000000000000c5e2e9'; - } - public static function getClientIdName(): string { return 'Client ID, also known as Customer Key'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yandex/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yandex/Update.php index 8e5e5839a8..f9af92408d 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yandex/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Yandex/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_YANDEX; } - public static function getClientIdDescription(): string - { - return '\'ClientID\' of Yandex OAuth2 app. For example: 6a8a6a0000000000000000000091483c'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client secret\' of Yandex OAuth2 app. For example: bbf98500000000000000000000c75a63'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoho/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoho/Update.php index 75fa3692bd..bcb30839ac 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoho/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoho/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_ZOHO; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Zoho OAuth2 app. For example: 1000.83C178000000000000000000RPNX0B'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of Zoho OAuth2 app. For example: fb5cac000000000000000000000000000000a68f6e'; - } - public static function getClientIdName(): string { return 'Client ID'; diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoom/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoom/Update.php index b0e999b256..d67cb4dba3 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoom/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Zoom/Update.php @@ -33,16 +33,6 @@ class Update extends Base return Response::MODEL_OAUTH2_ZOOM; } - public static function getClientIdDescription(): string - { - return '\'Client ID\' of Zoom OAuth2 app. For example: QMAC00000000000000w0AQ'; - } - - public static function getClientSecretDescription(): string - { - return '\'Client Secret\' of Zoom OAuth2 app. For example: GAWsG4000000000000000000007U01ON'; - } - public static function getClientIdName(): string { return 'Client ID'; From 543765a22ae2284afc163c171a250e90f7c6684f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 10:15:45 +0200 Subject: [PATCH 48/51] Improve copy --- .../Modules/Project/Http/Project/OAuth2/GitHub/Update.php | 2 +- .../Modules/Project/Http/Project/OAuth2/Microsoft/Update.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php index a82b3a3ea2..3b6f89db06 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php @@ -35,7 +35,7 @@ class Update extends Base public static function getClientIdName(): string { - return 'Client ID or App ID'; + return 'OAuth 2 app Client ID, or App ID'; } public static function getClientIdExample(): string diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php index fe4f4b263e..0690ee333a 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Microsoft/Update.php @@ -56,7 +56,7 @@ class Update extends Base public static function getClientIdName(): string { - return 'Application ID (also known as Client ID)'; + return 'Entra ID Application ID, also known as Client ID'; } public static function getClientIdExample(): string @@ -66,7 +66,7 @@ class Update extends Base public static function getClientSecretName(): string { - return 'Application Secret (also known as Client Secret)'; + return 'Entra ID Application Secret, also known as Client Secret'; } public static function getClientSecretExample(): string From dfa3ae52747bc22215a6f45441358d029d64cb94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 10:19:36 +0200 Subject: [PATCH 49/51] Fix tests --- tests/e2e/Services/Console/ConsoleConsoleClientTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php index 779ede8d9c..3b3232cda3 100644 --- a/tests/e2e/Services/Console/ConsoleConsoleClientTest.php +++ b/tests/e2e/Services/Console/ConsoleConsoleClientTest.php @@ -99,7 +99,7 @@ class ConsoleConsoleClientTest extends Scope $this->assertCount(2, $github['parameters']); $clientId = $github['parameters'][0]; $this->assertEquals('clientId', $clientId['$id']); - $this->assertEquals('Client ID or App ID', $clientId['name']); + $this->assertEquals('OAuth 2 app Client ID, or App ID', $clientId['name']); $this->assertEquals('e4d87900000000540733', $clientId['example']); $this->assertEquals('Example of wrong value: 370006', $clientId['hint']); $clientSecret = $github['parameters'][1]; From 49e6a38e7fe337a585eab8712fe01ea61a98089d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 10:43:16 +0200 Subject: [PATCH 50/51] Add fusionauth oauth --- app/config/oAuthProviders.php | 11 + app/init/models.php | 2 + src/Appwrite/Auth/OAuth2/FusionAuth.php | 226 ++++++++++++++++++ .../Project/Http/Project/OAuth2/Base.php | 1 + .../Http/Project/OAuth2/FusionAuth/Update.php | 172 +++++++++++++ .../Project/Http/Project/OAuth2/Get.php | 1 + .../Modules/Project/Services/Http.php | 2 + src/Appwrite/Utopia/Response.php | 1 + .../Response/Model/OAuth2FusionAuth.php | 59 +++++ .../Response/Model/OAuth2ProviderList.php | 1 + tests/e2e/Services/Project/OAuth2Base.php | 133 ++++++++++- 11 files changed, 604 insertions(+), 5 deletions(-) create mode 100644 src/Appwrite/Auth/OAuth2/FusionAuth.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/FusionAuth/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2FusionAuth.php diff --git a/app/config/oAuthProviders.php b/app/config/oAuthProviders.php index 0dc2cb8b1e..3b490bd153 100644 --- a/app/config/oAuthProviders.php +++ b/app/config/oAuthProviders.php @@ -167,6 +167,17 @@ return [ 'mock' => false, 'class' => 'Appwrite\\Auth\\OAuth2\\Figma', ], + 'fusionauth' => [ + 'name' => 'FusionAuth', + 'developers' => 'https://fusionauth.io/docs/', + 'icon' => 'icon-fusionauth', + 'enabled' => true, + 'sandbox' => false, + 'form' => 'fusionauth.phtml', + 'beta' => false, + 'mock' => false, + 'class' => 'Appwrite\\Auth\\OAuth2\\FusionAuth', + ], 'github' => [ 'name' => 'GitHub', 'developers' => 'https://developer.github.com/', diff --git a/app/init/models.php b/app/init/models.php index 39bc90e23c..ab397d6fdf 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -123,6 +123,7 @@ use Appwrite\Utopia\Response\Model\OAuth2Dropbox; use Appwrite\Utopia\Response\Model\OAuth2Etsy; use Appwrite\Utopia\Response\Model\OAuth2Facebook; use Appwrite\Utopia\Response\Model\OAuth2Figma; +use Appwrite\Utopia\Response\Model\OAuth2FusionAuth; use Appwrite\Utopia\Response\Model\OAuth2GitHub; use Appwrite\Utopia\Response\Model\OAuth2Gitlab; use Appwrite\Utopia\Response\Model\OAuth2Google; @@ -425,6 +426,7 @@ Response::setModel(new OAuth2Paypal()); Response::setModel(new OAuth2Gitlab()); Response::setModel(new OAuth2Authentik()); Response::setModel(new OAuth2Auth0()); +Response::setModel(new OAuth2FusionAuth()); Response::setModel(new OAuth2Oidc()); Response::setModel(new OAuth2Okta()); Response::setModel(new OAuth2Kick()); diff --git a/src/Appwrite/Auth/OAuth2/FusionAuth.php b/src/Appwrite/Auth/OAuth2/FusionAuth.php new file mode 100644 index 0000000000..415be4c6ad --- /dev/null +++ b/src/Appwrite/Auth/OAuth2/FusionAuth.php @@ -0,0 +1,226 @@ +getFusionAuthDomain() . '/oauth2/authorize?' . \http_build_query([ + 'client_id' => $this->appID, + 'redirect_uri' => $this->callback, + 'state' => \json_encode($this->state), + 'scope' => \implode(' ', $this->getScopes()), + 'response_type' => 'code' + ]); + } + + /** + * @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://' . $this->getFusionAuthDomain() . '/oauth2/token', + $headers, + \http_build_query([ + 'code' => $code, + 'client_id' => $this->appID, + 'client_secret' => $this->getClientSecret(), + 'redirect_uri' => $this->callback, + 'scope' => \implode(' ', $this->getScopes()), + 'grant_type' => 'authorization_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://' . $this->getFusionAuthDomain() . '/oauth2/token', + $headers, + \http_build_query([ + 'refresh_token' => $refreshToken, + 'client_id' => $this->appID, + 'client_secret' => $this->getClientSecret(), + 'grant_type' => 'refresh_token' + ]) + ), 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); + + if (isset($user['sub'])) { + return $user['sub']; + } + + return ''; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserEmail(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if (isset($user['email'])) { + return $user['email']; + } + + return ''; + } + + /** + * Check if the User email is verified + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if ($user['email_verified'] ?? false) { + return true; + } + + return false; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserName(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if (isset($user['name'])) { + return $user['name']; + } + + return ''; + } + + /** + * @param string $accessToken + * + * @return array + */ + protected function getUser(string $accessToken): array + { + if (empty($this->user)) { + $headers = ['Authorization: Bearer ' . \urlencode($accessToken)]; + $user = $this->request('GET', 'https://' . $this->getFusionAuthDomain() . '/oauth2/userinfo', $headers); + $this->user = \json_decode($user, true); + } + + return $this->user; + } + + /** + * Extracts the Client Secret from the JSON stored in appSecret + * + * @return string + */ + protected function getClientSecret(): string + { + $secret = $this->getAppSecret(); + + return $secret['clientSecret'] ?? ''; + } + + /** + * Extracts the FusionAuth Domain from the JSON stored in appSecret + * + * @return string + */ + protected function getFusionAuthDomain(): string + { + $secret = $this->getAppSecret(); + return $secret['fusionAuthDomain'] ?? ''; + } + + /** + * Decode the JSON stored in appSecret + * + * @return array + */ + protected function getAppSecret(): array + { + try { + $secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR); + } catch (\Throwable $th) { + throw new \Exception('Invalid secret'); + } + return $secret; + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php index b0f59e7c08..3925abb582 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php @@ -311,6 +311,7 @@ abstract class Base extends Action 'gitlab' => Gitlab\Update::class, 'authentik' => Authentik\Update::class, 'auth0' => Auth0\Update::class, + 'fusionauth' => FusionAuth\Update::class, 'oidc' => Oidc\Update::class, 'okta' => Okta\Update::class, 'kick' => Kick\Update::class, diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/FusionAuth/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/FusionAuth/Update.php new file mode 100644 index 0000000000..25f81e1459 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/FusionAuth/Update.php @@ -0,0 +1,172 @@ + 'endpoint', + 'name' => 'Domain', + 'example' => 'example.fusionauth.io', + 'hint' => '', + ], + ]); + } + + 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: static::getProviderSDKMethod(), + description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: static::getResponseModel(), + ) + ], + )) + ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) + ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) + ->param('endpoint', '', new Text(256, 1), 'Domain of FusionAuth instance. For example: example.fusionauth.io', optional: false) + ->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') + ->inject('queueForEvents') + ->callback($this->handle(...)); + } + + public function buildReadResponse(Document $project): Document + { + $providerId = static::getProviderId(); + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $decoded = $this->decodeStoredSecret($project); + + return new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => '', + 'endpoint' => $decoded['fusionAuthDomain'] ?? '', + ]); + } + + /** + * Custom callback used instead of the parent's `action()` because FusionAuth + * takes an additional required `endpoint` parameter. The method is named + * differently to avoid an LSP-incompatible override of Base::action(). + */ + public function handle( + ?string $clientId, + ?string $clientSecret, + string $endpoint, + ?bool $enabled, + Response $response, + Database $dbForPlatform, + Document $project, + Authorization $authorization, + QueueEvent $queueForEvents + ): void { + $providerId = static::getProviderId(); + $queueForEvents->setParam('providerId', $providerId); + + // The secret is stored as JSON `{"clientSecret": "...", "fusionAuthDomain": "..."}` + // to match the shape FusionAuth's OAuth2 adapter expects (getFusionAuthDomain()). + // The `endpoint` param is required on every call, so it's always written. + // `clientSecret` is optional; if omitted, the existing stored secret is preserved. + $storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? ''; + $existing = []; + if (!empty($storedRaw)) { + $existing = \json_decode($storedRaw, true) ?: []; + } + $encodedSecret = \json_encode([ + 'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''), + 'fusionAuthDomain' => $endpoint, + ]); + + $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled); + + // Reuse buildReadResponse to keep PATCH/GET shapes identical and + // guarantee the clientSecret is write-only on every response path. + $response->dynamic($this->buildReadResponse($project), static::getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php index 419d80f829..0e10a8841c 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php @@ -75,6 +75,7 @@ class Get extends Action Response::MODEL_OAUTH2_GITLAB, Response::MODEL_OAUTH2_AUTHENTIK, Response::MODEL_OAUTH2_AUTH0, + Response::MODEL_OAUTH2_FUSIONAUTH, Response::MODEL_OAUTH2_OIDC, Response::MODEL_OAUTH2_APPLE, Response::MODEL_OAUTH2_OKTA, diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index d6ff3c4925..76dbf58ef8 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -31,6 +31,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Dropbox\Update as Upda use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Etsy\Update as UpdateOAuth2Etsy; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Facebook\Update as UpdateOAuth2Facebook; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Figma\Update as UpdateOAuth2Figma; +use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\FusionAuth\Update as UpdateOAuth2FusionAuth; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Get as GetOAuth2Provider; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\GitHub\Update as UpdateOAuth2GitHub; use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Gitlab\Update as UpdateOAuth2Gitlab; @@ -210,6 +211,7 @@ class Http extends Service $this->addAction(UpdateOAuth2Gitlab::getName(), new UpdateOAuth2Gitlab()); $this->addAction(UpdateOAuth2Authentik::getName(), new UpdateOAuth2Authentik()); $this->addAction(UpdateOAuth2Auth0::getName(), new UpdateOAuth2Auth0()); + $this->addAction(UpdateOAuth2FusionAuth::getName(), new UpdateOAuth2FusionAuth()); $this->addAction(UpdateOAuth2Oidc::getName(), new UpdateOAuth2Oidc()); $this->addAction(UpdateOAuth2Okta::getName(), new UpdateOAuth2Okta()); $this->addAction(UpdateOAuth2Kick::getName(), new UpdateOAuth2Kick()); diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 7670b027e9..14bfbdb9ef 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -311,6 +311,7 @@ class Response extends SwooleResponse public const MODEL_OAUTH2_GITLAB = 'oAuth2Gitlab'; public const MODEL_OAUTH2_AUTHENTIK = 'oAuth2Authentik'; public const MODEL_OAUTH2_AUTH0 = 'oAuth2Auth0'; + public const MODEL_OAUTH2_FUSIONAUTH = 'oAuth2FusionAuth'; public const MODEL_OAUTH2_OIDC = 'oAuth2Oidc'; public const MODEL_OAUTH2_APPLE = 'oAuth2Apple'; public const MODEL_OAUTH2_OKTA = 'oAuth2Okta'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2FusionAuth.php b/src/Appwrite/Utopia/Response/Model/OAuth2FusionAuth.php new file mode 100644 index 0000000000..8dbe3c76f0 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2FusionAuth.php @@ -0,0 +1,59 @@ + 'fusionauth', + ]; + + public function getProviderLabel(): string + { + return 'FusionAuth'; + } + + public function getClientIdExample(): string + { + return 'b2222c00-0000-0000-0000-000000862097'; + } + + public function getClientSecretExample(): string + { + return 'Jx4s0C0000000000000000000000000000000wGqLsc'; + } + + public function __construct() + { + parent::__construct(); + + $this->addRule('endpoint', [ + 'type' => self::TYPE_STRING, + 'description' => 'FusionAuth OAuth2 endpoint domain.', + 'default' => '', + 'example' => 'example.fusionauth.io', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'OAuth2FusionAuth'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_OAUTH2_FUSIONAUTH; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php b/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php index 5d1fb16a9a..71cf5ed2eb 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php @@ -51,6 +51,7 @@ class OAuth2ProviderList extends Model Response::MODEL_OAUTH2_GITLAB, Response::MODEL_OAUTH2_AUTHENTIK, Response::MODEL_OAUTH2_AUTH0, + Response::MODEL_OAUTH2_FUSIONAUTH, Response::MODEL_OAUTH2_OIDC, Response::MODEL_OAUTH2_APPLE, Response::MODEL_OAUTH2_OKTA, diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index ec070531e7..5cb1b7b0c4 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -64,6 +64,7 @@ trait OAuth2Base 'apple', 'auth0', 'authentik', + 'fusionauth', 'gitlab', 'oidc', 'okta', @@ -95,11 +96,11 @@ trait OAuth2Base $expected = [ 'amazon', 'apple', 'auth0', 'authentik', 'autodesk', 'bitbucket', 'bitly', 'box', 'dailymotion', 'discord', 'disqus', 'dropbox', - 'etsy', 'facebook', 'figma', 'github', 'gitlab', 'google', 'kick', - 'linkedin', 'microsoft', 'notion', 'oidc', 'okta', 'paypal', - 'paypalSandbox', 'podio', 'salesforce', 'slack', 'spotify', - 'stripe', 'tradeshift', 'tradeshiftBox', 'twitch', 'wordpress', - 'x', 'yahoo', 'yandex', 'zoho', 'zoom', + 'etsy', 'facebook', 'figma', 'fusionauth', 'github', 'gitlab', + 'google', 'kick', 'linkedin', 'microsoft', 'notion', 'oidc', + 'okta', 'paypal', 'paypalSandbox', 'podio', 'salesforce', 'slack', + 'spotify', 'stripe', 'tradeshift', 'tradeshiftBox', 'twitch', + 'wordpress', 'x', 'yahoo', 'yandex', 'zoho', 'zoom', ]; \sort($expected); @@ -995,6 +996,128 @@ trait OAuth2Base ]); } + // ========================================================================= + // Update FusionAuth (clientId + clientSecret + REQUIRED endpoint) + // ========================================================================= + + public function testUpdateOAuth2FusionAuthRequiresEndpoint(): void + { + // The `endpoint` param is required (Text(min=1)); omitting → 400. + $response = $this->updateOAuth2('fusionauth', [ + 'clientId' => 'whatever', + 'clientSecret' => 'whatever', + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + } + + public function testUpdateOAuth2FusionAuthEmptyEndpointRejected(): void + { + // The `endpoint` validator is Text(min=1). Sending `''` must be + // rejected the same way as omitting — the validator should treat the + // empty-string degenerate case as a missing required field. + $response = $this->updateOAuth2('fusionauth', [ + 'clientId' => 'whatever', + 'clientSecret' => 'whatever', + 'endpoint' => '', + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + } + + public function testUpdateOAuth2FusionAuth(): void + { + $response = $this->updateOAuth2('fusionauth', [ + 'clientId' => 'b2222c00-0000-0000-0000-000000862097', + 'clientSecret' => 'fusionauth-secret', + 'endpoint' => 'example.fusionauth.io', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('fusionauth', $response['body']['$id']); + $this->assertSame('b2222c00-0000-0000-0000-000000862097', $response['body']['clientId']); + $this->assertSame('example.fusionauth.io', $response['body']['endpoint']); + + // Cleanup + $this->updateOAuth2('fusionauth', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => 'cleanup.fusionauth.io', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2FusionAuthPartialPreservesSecret(): void + { + // FusionAuth's `endpoint` is required on every call, so we always + // re-send it. The `clientSecret` lives in the JSON blob and must + // survive when omitted on a subsequent call that only changes clientId. + $this->updateOAuth2('fusionauth', [ + 'clientId' => 'fusionauth-merge-client', + 'clientSecret' => 'fusionauth-merge-secret', + 'endpoint' => 'merge.fusionauth.io', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('fusionauth', [ + 'clientId' => 'fusionauth-rotated-client', + 'endpoint' => 'merge.fusionauth.io', + ]); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('fusionauth-rotated-client', $response['body']['clientId']); + $this->assertSame('merge.fusionauth.io', $response['body']['endpoint']); + + // Confirm clientSecret survived the omitted-field merge by enabling + // — FusionAuth has no verifyCredentials() hook, so non-empty stored + // secret is enough. `endpoint` must be re-sent (required on enable too). + $enable = $this->updateOAuth2('fusionauth', [ + 'endpoint' => 'merge.fusionauth.io', + 'enabled' => true, + ]); + $this->assertSame(200, $enable['headers']['status-code']); + $this->assertTrue($enable['body']['enabled']); + + // Cleanup — endpoint is required, use a placeholder. + $this->updateOAuth2('fusionauth', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => 'cleanup.fusionauth.io', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2FusionAuthEnableAndReadBack(): void + { + $update = $this->updateOAuth2('fusionauth', [ + 'clientId' => 'fusionauth-enable-client', + 'clientSecret' => 'fusionauth-enable-secret', + 'endpoint' => 'enable.fusionauth.io', + 'enabled' => true, + ]); + + $this->assertSame(200, $update['headers']['status-code']); + $this->assertTrue($update['body']['enabled']); + + // GET must hide clientSecret while keeping clientId and endpoint. + $get = $this->getOAuth2Provider('fusionauth'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertTrue($get['body']['enabled']); + $this->assertSame('fusionauth-enable-client', $get['body']['clientId']); + $this->assertSame('enable.fusionauth.io', $get['body']['endpoint']); + $this->assertSame('', $get['body']['clientSecret']); + + // Cleanup — endpoint is required (Text(min=1)) so use a placeholder. + $this->updateOAuth2('fusionauth', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => 'cleanup.fusionauth.io', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Microsoft (applicationId + applicationSecret + REQUIRED tenant) // ========================================================================= From cb4cff120b7a0ed064535cd6ac7e55623002810d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 10:54:13 +0200 Subject: [PATCH 51/51] Add Keycloak oauth support --- app/config/oAuthProviders.php | 11 + app/init/models.php | 2 + src/Appwrite/Auth/OAuth2/Keycloak.php | 249 ++++++++++++++++++ .../Project/Http/Project/OAuth2/Base.php | 1 + .../Project/Http/Project/OAuth2/Get.php | 1 + .../Http/Project/OAuth2/Keycloak/Update.php | 183 +++++++++++++ .../Modules/Project/Services/Http.php | 2 + src/Appwrite/Utopia/Response.php | 1 + .../Utopia/Response/Model/OAuth2Keycloak.php | 66 +++++ .../Response/Model/OAuth2ProviderList.php | 1 + tests/e2e/Services/Project/OAuth2Base.php | 174 +++++++++++- 11 files changed, 687 insertions(+), 4 deletions(-) create mode 100644 src/Appwrite/Auth/OAuth2/Keycloak.php create mode 100644 src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Keycloak/Update.php create mode 100644 src/Appwrite/Utopia/Response/Model/OAuth2Keycloak.php diff --git a/app/config/oAuthProviders.php b/app/config/oAuthProviders.php index 3b490bd153..3b492fd8bf 100644 --- a/app/config/oAuthProviders.php +++ b/app/config/oAuthProviders.php @@ -211,6 +211,17 @@ return [ 'mock' => false, 'class' => 'Appwrite\\Auth\\OAuth2\\Google', ], + 'keycloak' => [ + 'name' => 'Keycloak', + 'developers' => 'https://www.keycloak.org/documentation', + 'icon' => 'icon-keycloak', + 'enabled' => true, + 'sandbox' => false, + 'form' => 'keycloak.phtml', + 'beta' => false, + 'mock' => false, + 'class' => 'Appwrite\\Auth\\OAuth2\\Keycloak', + ], 'kick' => [ 'name' => 'Kick', 'developers' => 'https://docs.kick.com/', diff --git a/app/init/models.php b/app/init/models.php index ab397d6fdf..77ca9be451 100644 --- a/app/init/models.php +++ b/app/init/models.php @@ -127,6 +127,7 @@ use Appwrite\Utopia\Response\Model\OAuth2FusionAuth; use Appwrite\Utopia\Response\Model\OAuth2GitHub; use Appwrite\Utopia\Response\Model\OAuth2Gitlab; use Appwrite\Utopia\Response\Model\OAuth2Google; +use Appwrite\Utopia\Response\Model\OAuth2Keycloak; use Appwrite\Utopia\Response\Model\OAuth2Kick; use Appwrite\Utopia\Response\Model\OAuth2Linkedin; use Appwrite\Utopia\Response\Model\OAuth2Microsoft; @@ -427,6 +428,7 @@ Response::setModel(new OAuth2Gitlab()); Response::setModel(new OAuth2Authentik()); Response::setModel(new OAuth2Auth0()); Response::setModel(new OAuth2FusionAuth()); +Response::setModel(new OAuth2Keycloak()); Response::setModel(new OAuth2Oidc()); Response::setModel(new OAuth2Okta()); Response::setModel(new OAuth2Kick()); diff --git a/src/Appwrite/Auth/OAuth2/Keycloak.php b/src/Appwrite/Auth/OAuth2/Keycloak.php new file mode 100644 index 0000000000..05e007eb7d --- /dev/null +++ b/src/Appwrite/Auth/OAuth2/Keycloak.php @@ -0,0 +1,249 @@ +getRealmBaseURL() . '/protocol/openid-connect/auth?' . \http_build_query([ + 'client_id' => $this->appID, + 'redirect_uri' => $this->callback, + 'state' => \json_encode($this->state), + 'scope' => \implode(' ', $this->getScopes()), + 'response_type' => 'code' + ]); + } + + /** + * @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', + $this->getRealmBaseURL() . '/protocol/openid-connect/token', + $headers, + \http_build_query([ + 'code' => $code, + 'client_id' => $this->appID, + 'client_secret' => $this->getClientSecret(), + 'redirect_uri' => $this->callback, + 'scope' => \implode(' ', $this->getScopes()), + 'grant_type' => 'authorization_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', + $this->getRealmBaseURL() . '/protocol/openid-connect/token', + $headers, + \http_build_query([ + 'refresh_token' => $refreshToken, + 'client_id' => $this->appID, + 'client_secret' => $this->getClientSecret(), + 'grant_type' => 'refresh_token' + ]) + ), 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); + + if (isset($user['sub'])) { + return $user['sub']; + } + + return ''; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserEmail(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if (isset($user['email'])) { + return $user['email']; + } + + return ''; + } + + /** + * Check if the User email is verified + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if ($user['email_verified'] ?? false) { + return true; + } + + return false; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserName(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if (isset($user['name'])) { + return $user['name']; + } + + return ''; + } + + /** + * @param string $accessToken + * + * @return array + */ + protected function getUser(string $accessToken): array + { + if (empty($this->user)) { + $headers = ['Authorization: Bearer ' . \urlencode($accessToken)]; + $user = $this->request('GET', $this->getRealmBaseURL() . '/protocol/openid-connect/userinfo', $headers); + $this->user = \json_decode($user, true); + } + + return $this->user; + } + + /** + * Extracts the Client Secret from the JSON stored in appSecret + * + * @return string + */ + protected function getClientSecret(): string + { + $secret = $this->getAppSecret(); + + return $secret['clientSecret'] ?? ''; + } + + /** + * Extracts the Keycloak Domain from the JSON stored in appSecret + * + * @return string + */ + protected function getKeycloakDomain(): string + { + $secret = $this->getAppSecret(); + return $secret['keycloakDomain'] ?? ''; + } + + /** + * Extracts the Keycloak Realm from the JSON stored in appSecret + * + * @return string + */ + protected function getKeycloakRealm(): string + { + $secret = $this->getAppSecret(); + return $secret['keycloakRealm'] ?? ''; + } + + /** + * Build the realm-scoped base URL: `https://{domain}/realms/{realm}`. + * Keycloak realm names allow spaces and other characters that must be + * percent-encoded in URLs (e.g. `my realm` → `my%20realm`). + * + * @return string + */ + protected function getRealmBaseURL(): string + { + return 'https://' . $this->getKeycloakDomain() . '/realms/' . \rawurlencode($this->getKeycloakRealm()); + } + + /** + * Decode the JSON stored in appSecret + * + * @return array + */ + protected function getAppSecret(): array + { + try { + $secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR); + } catch (\Throwable $th) { + throw new \Exception('Invalid secret'); + } + return $secret; + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php index 3925abb582..b5b8cacb73 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Base.php @@ -312,6 +312,7 @@ abstract class Base extends Action 'authentik' => Authentik\Update::class, 'auth0' => Auth0\Update::class, 'fusionauth' => FusionAuth\Update::class, + 'keycloak' => Keycloak\Update::class, 'oidc' => Oidc\Update::class, 'okta' => Okta\Update::class, 'kick' => Kick\Update::class, diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php index 0e10a8841c..ae46a59c67 100644 --- a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Get.php @@ -76,6 +76,7 @@ class Get extends Action Response::MODEL_OAUTH2_AUTHENTIK, Response::MODEL_OAUTH2_AUTH0, Response::MODEL_OAUTH2_FUSIONAUTH, + Response::MODEL_OAUTH2_KEYCLOAK, Response::MODEL_OAUTH2_OIDC, Response::MODEL_OAUTH2_APPLE, Response::MODEL_OAUTH2_OKTA, diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Keycloak/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Keycloak/Update.php new file mode 100644 index 0000000000..797875cab2 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/Keycloak/Update.php @@ -0,0 +1,183 @@ + 'endpoint', + 'name' => 'Domain', + 'example' => 'keycloak.example.com', + 'hint' => '', + ], + [ + '$id' => 'realmName', + 'name' => 'Realm name', + 'example' => 'appwrite-realm', + 'hint' => '', + ], + ]); + } + + 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: static::getProviderSDKMethod(), + description: 'Update the project OAuth2 ' . $providerLabel . ' configuration.', + auth: [AuthType::ADMIN, AuthType::KEY], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_OK, + model: static::getResponseModel(), + ) + ], + )) + ->param(static::getClientIdParamName(), null, new Nullable(new Text(256, 0)), static::getClientIdDescription(), optional: true) + ->param(static::getClientSecretParamName(), null, new Nullable(new Text(512, 0)), static::getClientSecretDescription(), optional: true) + ->param('endpoint', '', new Text(256, 1), 'Domain of Keycloak instance. For example: keycloak.example.com', optional: false) + ->param('realmName', '', new Text(256, 1), 'Keycloak realm name. For example: appwrite-realm', optional: false) + ->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') + ->inject('queueForEvents') + ->callback($this->handle(...)); + } + + public function buildReadResponse(Document $project): Document + { + $providerId = static::getProviderId(); + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + $decoded = $this->decodeStoredSecret($project); + + return new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$providerId . 'Enabled'] ?? false, + static::getClientIdParamName() => $oAuthProviders[$providerId . 'Appid'] ?? '', + static::getClientSecretParamName() => '', + 'endpoint' => $decoded['keycloakDomain'] ?? '', + 'realmName' => $decoded['keycloakRealm'] ?? '', + ]); + } + + /** + * Custom callback used instead of the parent's `action()` because Keycloak + * takes additional required `endpoint` and `realmName` parameters. The + * method is named differently to avoid an LSP-incompatible override of + * Base::action(). + */ + public function handle( + ?string $clientId, + ?string $clientSecret, + string $endpoint, + string $realmName, + ?bool $enabled, + Response $response, + Database $dbForPlatform, + Document $project, + Authorization $authorization, + QueueEvent $queueForEvents + ): void { + $providerId = static::getProviderId(); + $queueForEvents->setParam('providerId', $providerId); + + // The secret is stored as JSON `{"clientSecret": "...", "keycloakDomain": "...", "keycloakRealm": "..."}` + // to match the shape Keycloak's OAuth2 adapter expects (getKeycloakDomain(), getKeycloakRealm()). + // The `endpoint` and `realmName` params are required on every call, so they're always written. + // `clientSecret` is optional; if omitted, the existing stored secret is preserved. + $storedRaw = $project->getAttribute('oAuthProviders', [])[$providerId . 'Secret'] ?? ''; + $existing = []; + if (!empty($storedRaw)) { + $existing = \json_decode($storedRaw, true) ?: []; + } + $encodedSecret = \json_encode([ + 'clientSecret' => $clientSecret ?? ($existing['clientSecret'] ?? ''), + 'keycloakDomain' => $endpoint, + 'keycloakRealm' => $realmName, + ]); + + $project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $encodedSecret, $enabled); + + // Reuse buildReadResponse to keep PATCH/GET shapes identical and + // guarantee the clientSecret is write-only on every response path. + $response->dynamic($this->buildReadResponse($project), static::getResponseModel()); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index 76dbf58ef8..8c6b9da7e7 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -36,6 +36,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Get as GetOAuth2Provid 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\Keycloak\Update as UpdateOAuth2Keycloak; 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\Microsoft\Update as UpdateOAuth2Microsoft; @@ -212,6 +213,7 @@ class Http extends Service $this->addAction(UpdateOAuth2Authentik::getName(), new UpdateOAuth2Authentik()); $this->addAction(UpdateOAuth2Auth0::getName(), new UpdateOAuth2Auth0()); $this->addAction(UpdateOAuth2FusionAuth::getName(), new UpdateOAuth2FusionAuth()); + $this->addAction(UpdateOAuth2Keycloak::getName(), new UpdateOAuth2Keycloak()); $this->addAction(UpdateOAuth2Oidc::getName(), new UpdateOAuth2Oidc()); $this->addAction(UpdateOAuth2Okta::getName(), new UpdateOAuth2Okta()); $this->addAction(UpdateOAuth2Kick::getName(), new UpdateOAuth2Kick()); diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 14bfbdb9ef..e37e2c6043 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -312,6 +312,7 @@ class Response extends SwooleResponse public const MODEL_OAUTH2_AUTHENTIK = 'oAuth2Authentik'; public const MODEL_OAUTH2_AUTH0 = 'oAuth2Auth0'; public const MODEL_OAUTH2_FUSIONAUTH = 'oAuth2FusionAuth'; + public const MODEL_OAUTH2_KEYCLOAK = 'oAuth2Keycloak'; public const MODEL_OAUTH2_OIDC = 'oAuth2Oidc'; public const MODEL_OAUTH2_APPLE = 'oAuth2Apple'; public const MODEL_OAUTH2_OKTA = 'oAuth2Okta'; diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2Keycloak.php b/src/Appwrite/Utopia/Response/Model/OAuth2Keycloak.php new file mode 100644 index 0000000000..063f7d2a5c --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/OAuth2Keycloak.php @@ -0,0 +1,66 @@ + 'keycloak', + ]; + + public function getProviderLabel(): string + { + return 'Keycloak'; + } + + public function getClientIdExample(): string + { + return 'appwrite-o0000000st-app'; + } + + public function getClientSecretExample(): string + { + return 'jdjrJd00000000000000000000HUsaZO'; + } + + public function __construct() + { + parent::__construct(); + + $this->addRule('endpoint', [ + 'type' => self::TYPE_STRING, + 'description' => 'Keycloak OAuth2 endpoint domain.', + 'default' => '', + 'example' => 'keycloak.example.com', + ]); + + $this->addRule('realmName', [ + 'type' => self::TYPE_STRING, + 'description' => 'Keycloak OAuth2 realm name.', + 'default' => '', + 'example' => 'appwrite-realm', + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'OAuth2Keycloak'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_OAUTH2_KEYCLOAK; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php b/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php index 71cf5ed2eb..81c23c803c 100644 --- a/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php +++ b/src/Appwrite/Utopia/Response/Model/OAuth2ProviderList.php @@ -52,6 +52,7 @@ class OAuth2ProviderList extends Model Response::MODEL_OAUTH2_AUTHENTIK, Response::MODEL_OAUTH2_AUTH0, Response::MODEL_OAUTH2_FUSIONAUTH, + Response::MODEL_OAUTH2_KEYCLOAK, Response::MODEL_OAUTH2_OIDC, Response::MODEL_OAUTH2_APPLE, Response::MODEL_OAUTH2_OKTA, diff --git a/tests/e2e/Services/Project/OAuth2Base.php b/tests/e2e/Services/Project/OAuth2Base.php index 5cb1b7b0c4..8345bfab0a 100644 --- a/tests/e2e/Services/Project/OAuth2Base.php +++ b/tests/e2e/Services/Project/OAuth2Base.php @@ -66,6 +66,7 @@ trait OAuth2Base 'authentik', 'fusionauth', 'gitlab', + 'keycloak', 'oidc', 'okta', 'microsoft', @@ -97,10 +98,10 @@ trait OAuth2Base 'amazon', 'apple', 'auth0', 'authentik', 'autodesk', 'bitbucket', 'bitly', 'box', 'dailymotion', 'discord', 'disqus', 'dropbox', 'etsy', 'facebook', 'figma', 'fusionauth', 'github', 'gitlab', - 'google', 'kick', 'linkedin', 'microsoft', 'notion', 'oidc', - 'okta', 'paypal', 'paypalSandbox', 'podio', 'salesforce', 'slack', - 'spotify', 'stripe', 'tradeshift', 'tradeshiftBox', 'twitch', - 'wordpress', 'x', 'yahoo', 'yandex', 'zoho', 'zoom', + 'google', 'keycloak', 'kick', 'linkedin', 'microsoft', 'notion', + 'oidc', 'okta', 'paypal', 'paypalSandbox', 'podio', 'salesforce', + 'slack', 'spotify', 'stripe', 'tradeshift', 'tradeshiftBox', + 'twitch', 'wordpress', 'x', 'yahoo', 'yandex', 'zoho', 'zoom', ]; \sort($expected); @@ -1118,6 +1119,171 @@ trait OAuth2Base ]); } + // ========================================================================= + // Update Keycloak (clientId + clientSecret + REQUIRED endpoint + REQUIRED realmName) + // ========================================================================= + + public function testUpdateOAuth2KeycloakRequiresEndpoint(): void + { + // The `endpoint` param is required (Text(min=1)); omitting → 400. + $response = $this->updateOAuth2('keycloak', [ + 'clientId' => 'whatever', + 'clientSecret' => 'whatever', + 'realmName' => 'appwrite-realm', + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + } + + public function testUpdateOAuth2KeycloakEmptyEndpointRejected(): void + { + // The `endpoint` validator is Text(min=1). Sending `''` must be + // rejected the same way as omitting — the validator should treat the + // empty-string degenerate case as a missing required field. + $response = $this->updateOAuth2('keycloak', [ + 'clientId' => 'whatever', + 'clientSecret' => 'whatever', + 'endpoint' => '', + 'realmName' => 'appwrite-realm', + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + } + + public function testUpdateOAuth2KeycloakRequiresRealmName(): void + { + // The `realmName` param is required (Text(min=1)); omitting → 400. + $response = $this->updateOAuth2('keycloak', [ + 'clientId' => 'whatever', + 'clientSecret' => 'whatever', + 'endpoint' => 'keycloak.example.com', + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + } + + public function testUpdateOAuth2KeycloakEmptyRealmNameRejected(): void + { + // The `realmName` validator is Text(min=1). Sending `''` must be + // rejected the same way as omitting. + $response = $this->updateOAuth2('keycloak', [ + 'clientId' => 'whatever', + 'clientSecret' => 'whatever', + 'endpoint' => 'keycloak.example.com', + 'realmName' => '', + ]); + + $this->assertSame(400, $response['headers']['status-code']); + $this->assertSame('general_argument_invalid', $response['body']['type']); + } + + public function testUpdateOAuth2Keycloak(): void + { + $response = $this->updateOAuth2('keycloak', [ + 'clientId' => 'appwrite-o0000000st-app', + 'clientSecret' => 'keycloak-secret', + 'endpoint' => 'keycloak.example.com', + 'realmName' => 'appwrite-realm', + 'enabled' => false, + ]); + + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('keycloak', $response['body']['$id']); + $this->assertSame('appwrite-o0000000st-app', $response['body']['clientId']); + $this->assertSame('keycloak.example.com', $response['body']['endpoint']); + $this->assertSame('appwrite-realm', $response['body']['realmName']); + + // Cleanup + $this->updateOAuth2('keycloak', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => 'cleanup.keycloak.com', + 'realmName' => 'cleanup-realm', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2KeycloakPartialPreservesSecret(): void + { + // Keycloak's `endpoint` and `realmName` are required on every call, + // so we always re-send them. The `clientSecret` lives in the JSON + // blob and must survive when omitted on a subsequent call that only + // changes clientId. + $this->updateOAuth2('keycloak', [ + 'clientId' => 'keycloak-merge-client', + 'clientSecret' => 'keycloak-merge-secret', + 'endpoint' => 'merge.keycloak.com', + 'realmName' => 'merge-realm', + 'enabled' => false, + ]); + + $response = $this->updateOAuth2('keycloak', [ + 'clientId' => 'keycloak-rotated-client', + 'endpoint' => 'merge.keycloak.com', + 'realmName' => 'merge-realm', + ]); + $this->assertSame(200, $response['headers']['status-code']); + $this->assertSame('keycloak-rotated-client', $response['body']['clientId']); + $this->assertSame('merge.keycloak.com', $response['body']['endpoint']); + $this->assertSame('merge-realm', $response['body']['realmName']); + + // Confirm clientSecret survived the omitted-field merge by enabling + // — Keycloak has no verifyCredentials() hook, so non-empty stored + // secret is enough. `endpoint`/`realmName` must be re-sent (required + // on enable too). + $enable = $this->updateOAuth2('keycloak', [ + 'endpoint' => 'merge.keycloak.com', + 'realmName' => 'merge-realm', + 'enabled' => true, + ]); + $this->assertSame(200, $enable['headers']['status-code']); + $this->assertTrue($enable['body']['enabled']); + + // Cleanup — endpoint and realmName are required, use placeholders. + $this->updateOAuth2('keycloak', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => 'cleanup.keycloak.com', + 'realmName' => 'cleanup-realm', + 'enabled' => false, + ]); + } + + public function testUpdateOAuth2KeycloakEnableAndReadBack(): void + { + $update = $this->updateOAuth2('keycloak', [ + 'clientId' => 'keycloak-enable-client', + 'clientSecret' => 'keycloak-enable-secret', + 'endpoint' => 'enable.keycloak.com', + 'realmName' => 'enable-realm', + 'enabled' => true, + ]); + + $this->assertSame(200, $update['headers']['status-code']); + $this->assertTrue($update['body']['enabled']); + + // GET must hide clientSecret while keeping clientId, endpoint, realmName. + $get = $this->getOAuth2Provider('keycloak'); + $this->assertSame(200, $get['headers']['status-code']); + $this->assertTrue($get['body']['enabled']); + $this->assertSame('keycloak-enable-client', $get['body']['clientId']); + $this->assertSame('enable.keycloak.com', $get['body']['endpoint']); + $this->assertSame('enable-realm', $get['body']['realmName']); + $this->assertSame('', $get['body']['clientSecret']); + + // Cleanup — endpoint and realmName are required (Text(min=1)) so use placeholders. + $this->updateOAuth2('keycloak', [ + 'clientId' => '', + 'clientSecret' => '', + 'endpoint' => 'cleanup.keycloak.com', + 'realmName' => 'cleanup-realm', + 'enabled' => false, + ]); + } + // ========================================================================= // Update Microsoft (applicationId + applicationSecret + REQUIRED tenant) // =========================================================================