Merge branch '1.9.x' of https://github.com/appwrite/appwrite into feat-out-of-order-chunk-uploads

This commit is contained in:
Torsten Dittmann
2026-04-28 17:10:56 +04:00
123 changed files with 11036 additions and 36 deletions
@@ -0,0 +1,174 @@
# Parallel Chunk Upload Support for utopia-php/storage
## Context
The Appwrite API now supports out-of-order chunked uploads (chunks can arrive in any sequence). The next step is **parallel uploads** — multiple chunks uploaded simultaneously via separate HTTP requests. The SDK guarantees the first chunk is sent before any parallel chunks, so the document creation race is handled at the API layer. However, the storage device layer has a race condition that must be fixed.
## Problem: `Local::joinChunks()` Race
When two requests upload the final missing chunks in parallel, both can observe `countChunks() == $chunks` and call `joinChunks()` simultaneously.
### Current behavior (loser throws)
```php
// Local::joinChunks()
$dest = \fopen($tmpAssemble, 'wb');
// ... stream all parts into $tmpAssemble ...
if (! \rename($tmpAssemble, $path)) {
\unlink($tmpAssemble);
throw new Exception('Failed to finalize assembled file '.$path);
}
```
The winner succeeds with `rename()`. The loser gets `false` from `rename()` (file already exists at `$path`) and throws a 500-error exception. The client that lost the race receives an error even though the file is fully assembled.
### Required behavior
If `$path` already exists, another request already assembled the file. The loser should **silently succeed** — the file is complete, nothing more to do.
## Proposed Changes
### 1. `Local::joinChunks()` — Handle assembly race
Before opening `$tmpAssemble`, check if the final file already exists. If it does, skip assembly entirely.
```php
private function joinChunks(string $path, int $chunks): void
{
// Race winner already assembled the file
if (\file_exists($path)) {
return;
}
$tmp = \dirname($path).DIRECTORY_SEPARATOR.'tmp_'.asename($path);
$tmpAssemble = \dirname($path).DIRECTORY_SEPARATOR.'tmp_assemble_'.asename($path);
// ... rest of assembly logic ...
if (! \rename($tmpAssemble, $path)) {
// Another request may have won the race between fclose and rename
if (\file_exists($path)) {
\unlink($tmpAssemble);
return;
}
\unlink($tmpAssemble);
throw new Exception('Failed to finalize assembled file '.$path);
}
// ... cleanup ...
}
```
### 2. `Local::countChunks()` — Reliability under concurrent writes
`countChunks()` uses `glob()` on the temp directory. Under heavy parallel load, `glob()` might miss files or return inconsistent counts. The current implementation is already fairly robust (it validates `.part.\d+` suffix), but we should document that the return value is a best-effort snapshot.
No code change needed here unless tests reveal issues.
### 3. Tests — Concurrent chunk uploads
Add a test that simulates two parallel requests completing a multi-chunk upload:
```php
public function testParallelChunkUpload(): void
{
$storage = $this->makeJoinTestStorage();
$dest = $storage->getRoot().DIRECTORY_SEPARATOR.'parallel.dat';
// Upload chunk 1 (creates temp directory)
$storage->uploadData('AAAA', $dest, 'application/octet-stream', 1, 2);
// Simulate two parallel requests uploading the last chunk
// In a real test, use pcntl_fork() or pthreads for true concurrency
// For the test suite, sequential calls are sufficient if we verify
// the second call doesn't throw after the first completed assembly
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
// Verify file exists and is correct
$this->assertTrue(\file_exists($dest));
$this->assertSame('AAAABBBB', \file_get_contents($dest));
// Verify second assembly attempt doesn't throw
// (This simulates the race where another request already assembled)
try {
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
} catch (\Exception $e) {
$this->fail('Duplicate assembly should not throw: '.$e->getMessage());
}
$storage->delete($storage->getRoot(), true);
}
```
A more realistic concurrent test using `pcntl_fork()`:
```php
public function testParallelChunkUploadWithFork(): void
{
if (!\function_exists('pcntl_fork')) {
$this->markTestSkipped('pcntl extension required for fork-based concurrency test');
}
$storage = $this->makeJoinTestStorage();
$dest = $storage->getRoot().DIRECTORY_SEPARATOR.'parallel-fork.dat';
// Pre-upload chunk 1
$storage->uploadData('AAAA', $dest, 'application/octet-stream', 1, 2);
$pid = pcntl_fork();
if ($pid === -1) {
$this->fail('Failed to fork');
} elseif ($pid === 0) {
// Child process: upload chunk 2
try {
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
exit(0);
} catch (\Exception $e) {
exit(1);
}
}
// Parent process: also upload chunk 2 (race condition)
$parentSuccess = true;
try {
$storage->uploadData('BBBB', $dest, 'application/octet-stream', 2, 2);
} catch (\Exception $e) {
$parentSuccess = false;
}
pcntl_waitpid($pid, $status);
$childSuccess = pcntl_wexitstatus($status) === 0;
// At least one should succeed
$this->assertTrue($parentSuccess || $childSuccess, 'At least one parallel upload should succeed');
// File should be correctly assembled
$this->assertTrue(\file_exists($dest));
$this->assertSame('AAAABBBB', \file_get_contents($dest));
$storage->delete($storage->getRoot(), true);
}
```
## S3 Device
S3 already handles out-of-order multipart uploads natively. The `completeMultipartUpload` call with `ksort()` sorts parts by number regardless of upload order. However, parallel `completeMultipartUpload` calls for the same `uploadId` would still be problematic.
This is an **API-layer concern** — the Appwrite API should ensure only one request calls `completeMultipartUpload` per upload. The S3 device itself does not need changes.
## Files to Change
| File | Change |
|------|--------|
| `src/Storage/Device/Local.php` | Add `file_exists($path)` guard at start of `joinChunks()` and in `rename()` failure handler |
| `tests/Storage/Device/LocalTest.php` | Add `testParallelChunkUpload` and `testParallelChunkUploadWithFork` |
## Backwards Compatibility
Fully backwards compatible. The change only affects the error path when `rename()` fails due to an existing file. Previously it threw; now it returns silently. No public API signatures change.
## Related PRs
- Appwrite server PR: https://github.com/appwrite/appwrite/pull/12138 (out-of-order upload support)
- This storage PR is a prerequisite for the follow-up Appwrite PR that enables parallel chunk uploads at the API level.
+2
View File
@@ -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=
+5 -1
View File
@@ -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
@@ -526,6 +528,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
+33
View File
@@ -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/',
@@ -200,6 +211,28 @@ 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/',
'icon' => 'icon-kick',
'enabled' => true,
'sandbox' => false,
'form' => false,
'beta' => false,
'mock' => false,
'class' => 'Appwrite\\Auth\\OAuth2\\Kick',
],
'linkedin' => [
'name' => 'LinkedIn',
'developers' => 'https://developer.linkedin.com/',
+2
View File
@@ -55,6 +55,8 @@ $admins = [
'tables.write',
'platforms.read',
'platforms.write',
'oauth2.read',
'oauth2.write',
'mocks.read',
'mocks.write',
'policies.read',
+8
View File
@@ -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",
],
];
+1 -13
View File
@@ -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)
+1 -1
View File
@@ -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);
}
+88
View File
@@ -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;
@@ -105,6 +108,47 @@ 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\OAuth2Apple;
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;
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\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;
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\OAuth2ProviderList;
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\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;
@@ -350,6 +394,47 @@ Response::setModel(new Webhook());
Response::setModel(new Key());
Response::setModel(new DevKey());
Response::setModel(new MockNumber());
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 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 OAuth2Tradeshift());
Response::setModel(new OAuth2Paypal());
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());
Response::setModel(new OAuth2Apple());
Response::setModel(new OAuth2Microsoft());
Response::setModel(new OAuth2ProviderList());
Response::setModel(new PolicyPasswordDictionary());
Response::setModel(new PolicyPasswordHistory());
Response::setModel(new PolicyPasswordPersonalData());
@@ -398,6 +483,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());
+1
View File
@@ -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.*",
Generated
+17 -17
View File
@@ -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": "ba332fbec7c2e7d462ee5bb3fad9775c",
"content-hash": "4bee36b21a57e754d2b3417e72dc9599",
"packages": [
{
"name": "adhocore/jwt",
@@ -5183,16 +5183,16 @@
},
{
"name": "utopia-php/validators",
"version": "0.2.0",
"version": "0.2.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/validators.git",
"reference": "30b6030a5b100fc1dff34506e5053759594b2a20"
"reference": "5d7d494e64457cd4eb67fdcfd9481f2c89796aa6"
},
"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/5d7d494e64457cd4eb67fdcfd9481f2c89796aa6",
"reference": "5d7d494e64457cd4eb67fdcfd9481f2c89796aa6",
"shasum": ""
},
"require": {
@@ -5222,9 +5222,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.2"
},
"time": "2026-01-13T09:16:51+00:00"
"time": "2026-04-27T16:30:24+00:00"
},
{
"name": "utopia-php/vcs",
@@ -5465,16 +5465,16 @@
"packages-dev": [
{
"name": "appwrite/sdk-generator",
"version": "1.24.0",
"version": "1.25.1",
"source": {
"type": "git",
"url": "https://github.com/appwrite/sdk-generator.git",
"reference": "6d4d26659bc7a1c347c1d4d8dae3b77b5562e0cb"
"reference": "f21a556b9acdbf75bbdcdc90a078af641646eade"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6d4d26659bc7a1c347c1d4d8dae3b77b5562e0cb",
"reference": "6d4d26659bc7a1c347c1d4d8dae3b77b5562e0cb",
"url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/f21a556b9acdbf75bbdcdc90a078af641646eade",
"reference": "f21a556b9acdbf75bbdcdc90a078af641646eade",
"shasum": ""
},
"require": {
@@ -5510,9 +5510,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.24.0"
"source": "https://github.com/appwrite/sdk-generator/tree/1.25.1"
},
"time": "2026-04-24T12:50:05+00:00"
"time": "2026-04-28T11:12:22+00:00"
},
{
"name": "brianium/paratest",
@@ -6221,11 +6221,11 @@
},
{
"name": "phpstan/phpstan",
"version": "2.1.51",
"version": "2.1.52",
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59",
"reference": "dc3b523c45e714c70de2ac5113b958223b55dc59",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/08a34f8db7ca4daabff74a474fe13c0e56e2b4e5",
"reference": "08a34f8db7ca4daabff74a474fe13c0e56e2b4e5",
"shasum": ""
},
"require": {
@@ -6270,7 +6270,7 @@
"type": "github"
}
],
"time": "2026-04-21T18:22:01+00:00"
"time": "2026-04-28T12:17:53+00:00"
},
{
"name": "phpunit/php-code-coverage",
+2
View File
@@ -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"
@@ -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.
+226
View File
@@ -0,0 +1,226 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
// Reference Material
// https://fusionauth.io/docs/lifecycle/authenticate-users/oauth/endpoints
class FusionAuth extends OAuth2
{
/**
* @var array
*/
protected array $scopes = [
'openid',
'profile',
'email',
'offline_access'
];
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @return string
*/
public function getName(): string
{
return 'fusionauth';
}
/**
* @return string
*/
public function getLoginURL(): string
{
return 'https://' . $this->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;
}
}
+31
View File
@@ -3,6 +3,7 @@
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
use Utopia\Fetch\Client as FetchClient;
class Github extends OAuth2
{
@@ -219,4 +220,34 @@ class Github extends OAuth2
$repository = \json_decode($repository, true);
return $repository;
}
public function verifyCredentials(): void
{
$client = new FetchClient();
$client->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
}
}
+249
View File
@@ -0,0 +1,249 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
// Reference Material
// https://www.keycloak.org/docs/latest/securing_apps/#_oidc
class Keycloak extends OAuth2
{
/**
* @var array
*/
protected array $scopes = [
'openid',
'profile',
'email',
'offline_access'
];
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @return string
*/
public function getName(): string
{
return 'keycloak';
}
/**
* @return string
*/
public function getLoginURL(): string
{
return $this->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;
}
}
+230
View File
@@ -0,0 +1,230 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Appwrite\Auth\OAuth2;
// Reference Material
// https://docs.kick.com/getting-started/generating-tokens-oauth2-flow
// https://docs.kick.com/getting-started/scopes
class Kick extends OAuth2
{
private const PKCE_STATE_KEY = '_pkce';
/**
* @var array
*/
protected array $user = [];
/**
* @var array
*/
protected array $tokens = [];
/**
* @var array
*/
protected array $scopes = [
'user:read',
];
/**
* @var string
*/
private string $pkceVerifier = '';
/**
* @return string
*/
public function getName(): string
{
return 'kick';
}
/**
* @return string
*/
public function getLoginURL(): string
{
$state = $this->state;
$state[self::PKCE_STATE_KEY] = $this->getPKCEVerifier();
return 'https://id.kick.com/oauth/authorize?' . \http_build_query([
'response_type' => 'code',
'client_id' => $this->appID,
'redirect_uri' => $this->callback,
'scope' => \implode(' ', $this->getScopes()),
'state' => \json_encode($state),
'code_challenge' => $this->getPKCEChallenge(),
'code_challenge_method' => 'S256',
]);
}
/**
* @param string $code
*
* @return array
*/
protected function getTokens(string $code): array
{
if (empty($this->tokens)) {
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
'https://id.kick.com/oauth/token',
$headers,
\http_build_query([
'grant_type' => 'authorization_code',
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'redirect_uri' => $this->callback,
'code_verifier' => $this->getPKCEVerifier(),
'code' => $code,
])
), true);
}
return $this->tokens;
}
/**
* @param string $refreshToken
*
* @return array
*/
public function refreshTokens(string $refreshToken): array
{
$headers = ['Content-Type: application/x-www-form-urlencoded'];
$this->tokens = \json_decode($this->request(
'POST',
'https://id.kick.com/oauth/token',
$headers,
\http_build_query([
'grant_type' => 'refresh_token',
'client_id' => $this->appID,
'client_secret' => $this->appSecret,
'refresh_token' => $refreshToken,
])
), true);
if (empty($this->tokens['refresh_token'])) {
$this->tokens['refresh_token'] = $refreshToken;
}
return $this->tokens;
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserID(string $accessToken): string
{
$user = $this->getUser($accessToken);
return isset($user['user_id']) ? (string)$user['user_id'] : '';
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserEmail(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['email'] ?? '';
}
/**
* Check if the OAuth email is verified.
*
* Kick only returns an email when the user has granted the `user:read`
* scope and the account email is verified, so a non-empty email is
* treated as verified.
*
* @param string $accessToken
*
* @return bool
*/
public function isEmailVerified(string $accessToken): bool
{
return !empty($this->getUserEmail($accessToken));
}
/**
* @param string $accessToken
*
* @return string
*/
public function getUserName(string $accessToken): string
{
$user = $this->getUser($accessToken);
return $user['name'] ?? '';
}
/**
* @param string $accessToken
*
* @return array
*/
protected function getUser(string $accessToken): array
{
if (empty($this->user)) {
$headers = ['Authorization: Bearer ' . $accessToken];
$response = \json_decode($this->request(
'GET',
'https://api.kick.com/public/v1/users',
$headers
), true);
$this->user = $response['data'][0] ?? [];
}
return $this->user;
}
/**
* Extract the PKCE verifier from the state on the callback so the same
* value generated in getLoginURL() can be sent to the token endpoint.
*
* @param string $state
*
* @return array<string, mixed>|null
*/
public function parseState(string $state): ?array
{
$parsed = \json_decode($state, true);
if (!\is_array($parsed)) {
return null;
}
$verifier = $parsed[self::PKCE_STATE_KEY] ?? null;
if (\is_string($verifier)) {
$this->pkceVerifier = $verifier;
}
unset($parsed[self::PKCE_STATE_KEY]);
return $parsed;
}
private function getPKCEVerifier(): string
{
if ($this->pkceVerifier === '') {
$this->pkceVerifier = \rtrim(\strtr(\base64_encode(\random_bytes(64)), '+/', '-_'), '=');
}
return $this->pkceVerifier;
}
private function getPKCEChallenge(): string
{
return \rtrim(\strtr(\base64_encode(\hash('sha256', $this->getPKCEVerifier(), true)), '+/', '-_'), '=');
}
}
@@ -0,0 +1,80 @@
<?php
namespace Appwrite\Platform\Modules\Console\Http\OAuth2Providers;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base as OAuth2Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class XList extends Action
{
use HTTP;
public static function getName(): string
{
return 'listOAuth2Providers';
}
public function __construct()
{
$this
->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);
}
}
@@ -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());
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Amazon;
use Appwrite\Auth\OAuth2\Amazon;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'amazon';
}
public static function getProviderClass(): string
{
return Amazon::class;
}
public static function getProviderLabel(): string
{
return 'Amazon';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Amazon';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_AMAZON;
}
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';
}
}
@@ -0,0 +1,206 @@
<?php
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;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
{
public static function getProviderId(): string
{
return 'apple';
}
public static function getProviderClass(): string
{
return Apple::class;
}
public static function getProviderLabel(): string
{
return 'Apple';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Apple';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_APPLE;
}
public static function getClientIdParamName(): string
{
return 'serviceId';
}
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();
$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('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')
->inject('queueForEvents')
->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
* 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,
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().
// 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);
// 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());
}
}
@@ -0,0 +1,175 @@
<?php
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;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
{
public static function getProviderId(): string
{
return 'auth0';
}
public static function getProviderClass(): string
{
return Auth0::class;
}
public static function getProviderLabel(): string
{
return 'Auth0';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Auth0';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_AUTH0;
}
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();
$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', 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')
->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['auth0Domain'] ?? '',
]);
}
/**
* 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,
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()).
// 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);
// 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());
}
}
@@ -0,0 +1,172 @@
<?php
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;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
{
public static function getProviderId(): string
{
return 'authentik';
}
public static function getProviderClass(): string
{
return Authentik::class;
}
public static function getProviderLabel(): string
{
return 'Authentik';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Authentik';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_AUTHENTIK;
}
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();
$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 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')
->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['authentikDomain'] ?? '',
]);
}
/**
* 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,
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()).
// 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);
// 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());
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Autodesk;
use Appwrite\Auth\OAuth2\Autodesk;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'autodesk';
}
public static function getProviderClass(): string
{
return Autodesk::class;
}
public static function getProviderLabel(): string
{
return 'Autodesk';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Autodesk';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_AUTODESK;
}
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';
}
}
@@ -0,0 +1,449 @@
<?php
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;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
abstract class Base extends Action
{
use HTTP;
/**
* Provider ID used in paths, database keys and event labels.
*
* @return string e.g. 'github', 'discord', 'figma'
*/
abstract public static function getProviderId(): string;
/**
* Provider OAuth2 implementation class. Must implement verifyCredentials().
*
* @return class-string e.g. Github::class
*/
abstract public static function getProviderClass(): string;
/**
* Provider display label used in descriptions, SDK method name and action name.
*
* @return string e.g. 'GitHub', 'Discord', 'Figma'
*/
abstract public static function getProviderLabel(): string;
/**
* Response model constant for this provider.
*
* @return string e.g. Response::MODEL_OAUTH2_GITHUB
*/
abstract public static function getResponseModel(): string;
/**
* Description of the clientId param, 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).
*/
public static function getClientIdDescription(): string
{
return self::buildParamDescription(
static::getClientIdName(),
static::getClientIdExample(),
static::getClientIdHint()
);
}
/**
* 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).
*/
public static function getClientSecretDescription(): string
{
return self::buildParamDescription(
static::getClientSecretName(),
static::getClientSecretExample(),
static::getClientSecretHint()
);
}
/**
* Format a parameter description as
* "'<name>' of <providerLabel> OAuth2 app. For example: <example>[. <hint>]".
* 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
* 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<int, array<string, string>>
*/
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
* 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';
}
/**
* 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();
}
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('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->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<string, class-string<Base>>
*/
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,
'tradeshiftBox' => TradeshiftSandbox\Update::class,
'paypal' => Paypal\Update::class,
'paypalSandbox' => PaypalSandbox\Update::class,
'gitlab' => Gitlab\Update::class,
'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,
'apple' => Apple\Update::class,
'microsoft' => Microsoft\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
* 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
): 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.');
}
$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
]);
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,
QueueEvent $queueForEvents
): void {
$project = $this->persistCredentials($project, $dbForPlatform, $authorization, $clientId, $clientSecret, $enabled);
$queueForEvents->setParam('providerId', static::getProviderId());
// 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());
}
}
@@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Bitbucket;
use Appwrite\Auth\OAuth2\Bitbucket;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'bitbucket';
}
public static function getProviderClass(): string
{
return Bitbucket::class;
}
public static function getProviderLabel(): string
{
return 'Bitbucket';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Bitbucket';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_BITBUCKET;
}
public static function getClientIdParamName(): string
{
return 'key';
}
public static function getClientSecretParamName(): string
{
return 'secret';
}
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';
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Bitly;
use Appwrite\Auth\OAuth2\Bitly;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'bitly';
}
public static function getProviderClass(): string
{
return Bitly::class;
}
public static function getProviderLabel(): string
{
return 'Bitly';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Bitly';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_BITLY;
}
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';
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Box;
use Appwrite\Auth\OAuth2\Box;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'box';
}
public static function getProviderClass(): string
{
return Box::class;
}
public static function getProviderLabel(): string
{
return 'Box';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Box';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_BOX;
}
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';
}
}
@@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Dailymotion;
use Appwrite\Auth\OAuth2\Dailymotion;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'dailymotion';
}
public static function getProviderClass(): string
{
return Dailymotion::class;
}
public static function getProviderLabel(): string
{
return 'Dailymotion';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Dailymotion';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_DAILYMOTION;
}
public static function getClientIdParamName(): string
{
return 'apiKey';
}
public static function getClientSecretParamName(): string
{
return 'apiSecret';
}
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';
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Discord;
use Appwrite\Auth\OAuth2\Discord;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'discord';
}
public static function getProviderClass(): string
{
return Discord::class;
}
public static function getProviderLabel(): string
{
return 'Discord';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Discord';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_DISCORD;
}
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';
}
}
@@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Disqus;
use Appwrite\Auth\OAuth2\Disqus;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'disqus';
}
public static function getProviderClass(): string
{
return Disqus::class;
}
public static function getProviderLabel(): string
{
return 'Disqus';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Disqus';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_DISQUS;
}
public static function getClientIdParamName(): string
{
return 'publicKey';
}
public static function getClientSecretParamName(): string
{
return 'secretKey';
}
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';
}
}
@@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Dropbox;
use Appwrite\Auth\OAuth2\Dropbox;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'dropbox';
}
public static function getProviderClass(): string
{
return Dropbox::class;
}
public static function getProviderLabel(): string
{
return 'Dropbox';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Dropbox';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_DROPBOX;
}
public static function getClientIdParamName(): string
{
return 'appKey';
}
public static function getClientSecretParamName(): string
{
return 'appSecret';
}
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';
}
}
@@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Etsy;
use Appwrite\Auth\OAuth2\Etsy;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'etsy';
}
public static function getProviderClass(): string
{
return Etsy::class;
}
public static function getProviderLabel(): string
{
return 'Etsy';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Etsy';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_ETSY;
}
public static function getClientIdParamName(): string
{
return 'keyString';
}
public static function getClientSecretParamName(): string
{
return 'sharedSecret';
}
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';
}
}
@@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Facebook;
use Appwrite\Auth\OAuth2\Facebook;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'facebook';
}
public static function getProviderClass(): string
{
return Facebook::class;
}
public static function getProviderLabel(): string
{
return 'Facebook';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Facebook';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_FACEBOOK;
}
public static function getClientIdParamName(): string
{
return 'appId';
}
public static function getClientSecretParamName(): string
{
return 'appSecret';
}
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';
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Figma;
use Appwrite\Auth\OAuth2\Figma;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'figma';
}
public static function getProviderClass(): string
{
return Figma::class;
}
public static function getProviderLabel(): string
{
return 'Figma';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Figma';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_FIGMA;
}
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';
}
}
@@ -0,0 +1,172 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\FusionAuth;
use Appwrite\Auth\OAuth2\FusionAuth;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Platform\Action;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
{
public static function getProviderId(): string
{
return 'fusionauth';
}
public static function getProviderClass(): string
{
return FusionAuth::class;
}
public static function getProviderLabel(): string
{
return 'FusionAuth';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2FusionAuth';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_FUSIONAUTH;
}
public static function getClientIdName(): string
{
return 'Client ID';
}
public static function getClientIdExample(): string
{
return 'b2222c00-0000-0000-0000-000000862097';
}
public static function getClientSecretName(): string
{
return 'Client Secret';
}
public static function getClientSecretExample(): string
{
return 'Jx4s0C0000000000000000000000000000000wGqLsc';
}
public static function getParameters(): array
{
return \array_merge(parent::getParameters(), [
[
'$id' => '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());
}
}
@@ -0,0 +1,115 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\Text;
class Get extends Action
{
use HTTP;
public static function getName()
{
return 'getProjectOAuth2';
}
public function __construct()
{
$this
->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: <<<EOT
Get a single OAuth2 provider configuration. Credential fields (client secret, p8 file, key/team IDs) are write-only and always returned empty.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: [
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_FUSIONAUTH,
Response::MODEL_OAUTH2_KEYCLOAK,
Response::MODEL_OAUTH2_OIDC,
Response::MODEL_OAUTH2_APPLE,
Response::MODEL_OAUTH2_OKTA,
Response::MODEL_OAUTH2_KICK,
Response::MODEL_OAUTH2_MICROSOFT,
],
)
]
))
->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);
}
$actions = Base::getProviderActions();
if (!isset($actions[$provider])) {
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
$updateClass = $actions[$provider];
$action = new $updateClass();
$response->dynamic($action->buildReadResponse($project), $updateClass::getResponseModel());
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\GitHub;
use Appwrite\Auth\OAuth2\Github;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'github';
}
public static function getProviderClass(): string
{
return Github::class;
}
public static function getProviderLabel(): string
{
return 'GitHub';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2GitHub';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_GITHUB;
}
public static function getClientIdName(): string
{
return 'OAuth 2 app 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';
}
}
@@ -0,0 +1,186 @@
<?php
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;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
class Update extends Base
{
public static function getProviderId(): string
{
return 'gitlab';
}
public static function getProviderClass(): string
{
return Gitlab::class;
}
public static function getProviderLabel(): string
{
return 'Gitlab';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Gitlab';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_GITLAB;
}
public static function getClientIdParamName(): string
{
return 'applicationId';
}
public static function getClientSecretParamName(): string
{
return 'secret';
}
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();
$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', 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')
->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['endpoint'] ?? '',
]);
}
/**
* 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,
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().
// 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);
// 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());
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Google;
use Appwrite\Auth\OAuth2\Google;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'google';
}
public static function getProviderClass(): string
{
return Google::class;
}
public static function getProviderLabel(): string
{
return 'Google';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Google';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_GOOGLE;
}
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';
}
}
@@ -0,0 +1,183 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Keycloak;
use Appwrite\Auth\OAuth2\Keycloak;
use Appwrite\Event\Event as QueueEvent;
use Appwrite\Platform\Action;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
{
public static function getProviderId(): string
{
return 'keycloak';
}
public static function getProviderClass(): string
{
return Keycloak::class;
}
public static function getProviderLabel(): string
{
return 'Keycloak';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Keycloak';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_KEYCLOAK;
}
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 'jdjrJd00000000000000000000HUsaZO';
}
public static function getParameters(): array
{
return \array_merge(parent::getParameters(), [
[
'$id' => '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());
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Kick;
use Appwrite\Auth\OAuth2\Kick;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'kick';
}
public static function getProviderClass(): string
{
return Kick::class;
}
public static function getProviderLabel(): string
{
return 'Kick';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Kick';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_KICK;
}
public static function 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';
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Linkedin;
use Appwrite\Auth\OAuth2\Linkedin;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'linkedin';
}
public static function getProviderClass(): string
{
return Linkedin::class;
}
public static function getProviderLabel(): string
{
return 'Linkedin';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Linkedin';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_LINKEDIN;
}
public static function getClientSecretParamName(): string
{
return 'primaryClientSecret';
}
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==';
}
}
@@ -0,0 +1,182 @@
<?php
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;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
{
public static function getProviderId(): string
{
return 'microsoft';
}
public static function getProviderClass(): string
{
return Microsoft::class;
}
public static function getProviderLabel(): string
{
return 'Microsoft';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Microsoft';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_MICROSOFT;
}
public static function getClientIdParamName(): string
{
return 'applicationId';
}
public static function getClientSecretParamName(): string
{
return 'applicationSecret';
}
public static function getClientIdName(): string
{
return 'Entra ID Application ID, also known as Client ID';
}
public static function getClientIdExample(): string
{
return '00001111-aaaa-2222-bbbb-3333cccc4444';
}
public static function getClientSecretName(): string
{
return 'Entra ID 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();
$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('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')
->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() => '',
'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,
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()).
// 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);
// 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());
}
}
@@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Notion;
use Appwrite\Auth\OAuth2\Notion;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'notion';
}
public static function getProviderClass(): string
{
return Notion::class;
}
public static function getProviderLabel(): string
{
return 'Notion';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Notion';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_NOTION;
}
public static function getClientIdParamName(): string
{
return 'oauthClientId';
}
public static function getClientSecretParamName(): string
{
return 'oauthClientSecret';
}
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';
}
}
@@ -0,0 +1,230 @@
<?php
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;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
class Update extends Base
{
public static function getProviderId(): string
{
return 'oidc';
}
public static function getProviderClass(): string
{
return Oidc::class;
}
public static function getProviderLabel(): string
{
return 'Oidc';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Oidc';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_OIDC;
}
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();
$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('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')
->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() => '',
'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
* 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,
QueueEvent $queueForEvents
): void {
$providerId = static::getProviderId();
$queueForEvents->setParam('providerId', $providerId);
// 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);
// 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());
}
}
@@ -0,0 +1,198 @@
<?php
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;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Validator\Boolean;
use Utopia\Validator\Domain as ValidatorDomain;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Base
{
public static function getProviderId(): string
{
return 'okta';
}
public static function getProviderClass(): string
{
return Okta::class;
}
public static function getProviderLabel(): string
{
return 'Okta';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Okta';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_OKTA;
}
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();
$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('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')
->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() => '',
'domain' => $decoded['oktaDomain'] ?? '',
'authorizationServerId' => $decoded['authorizationServerId'] ?? '',
]);
}
/**
* 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,
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.
// 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);
// 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());
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Paypal;
use Appwrite\Auth\OAuth2\Paypal;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'paypal';
}
public static function getProviderClass(): string
{
return Paypal::class;
}
public static function getProviderLabel(): string
{
return 'Paypal';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Paypal';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_PAYPAL;
}
public static function getClientSecretParamName(): string
{
return 'secretKey';
}
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';
}
}
@@ -0,0 +1,29 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\PaypalSandbox;
use Appwrite\Auth\OAuth2\PaypalSandbox;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Paypal\Update as PaypalUpdate;
class Update extends PaypalUpdate
{
public static function getProviderId(): string
{
return 'paypalSandbox';
}
public static function getProviderClass(): string
{
return PaypalSandbox::class;
}
public static function getProviderLabel(): string
{
return 'PaypalSandbox';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2PaypalSandbox';
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Podio;
use Appwrite\Auth\OAuth2\Podio;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'podio';
}
public static function getProviderClass(): string
{
return Podio::class;
}
public static function getProviderLabel(): string
{
return 'Podio';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Podio';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_PODIO;
}
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';
}
}
@@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Salesforce;
use Appwrite\Auth\OAuth2\Salesforce;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'salesforce';
}
public static function getProviderClass(): string
{
return Salesforce::class;
}
public static function getProviderLabel(): string
{
return 'Salesforce';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Salesforce';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_SALESFORCE;
}
public static function getClientIdParamName(): string
{
return 'customerKey';
}
public static function getClientSecretParamName(): string
{
return 'customerSecret';
}
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';
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Slack;
use Appwrite\Auth\OAuth2\Slack;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'slack';
}
public static function getProviderClass(): string
{
return Slack::class;
}
public static function getProviderLabel(): string
{
return 'Slack';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Slack';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_SLACK;
}
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';
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Spotify;
use Appwrite\Auth\OAuth2\Spotify;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'spotify';
}
public static function getProviderClass(): string
{
return Spotify::class;
}
public static function getProviderLabel(): string
{
return 'Spotify';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Spotify';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_SPOTIFY;
}
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';
}
}
@@ -0,0 +1,60 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Stripe;
use Appwrite\Auth\OAuth2\Stripe;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'stripe';
}
public static function getProviderClass(): string
{
return Stripe::class;
}
public static function getProviderLabel(): string
{
return 'Stripe';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Stripe';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_STRIPE;
}
public static function getClientSecretParamName(): string
{
return 'apiSecretKey';
}
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';
}
}
@@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Tradeshift;
use Appwrite\Auth\OAuth2\Tradeshift;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'tradeshift';
}
public static function getProviderClass(): string
{
return Tradeshift::class;
}
public static function getProviderLabel(): string
{
return 'Tradeshift';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Tradeshift';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_TRADESHIFT;
}
public static function getClientIdParamName(): string
{
return 'oauth2ClientId';
}
public static function getClientSecretParamName(): string
{
return 'oauth2ClientSecret';
}
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';
}
}
@@ -0,0 +1,29 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\TradeshiftSandbox;
use Appwrite\Auth\OAuth2\TradeshiftBox;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Tradeshift\Update as TradeshiftUpdate;
class Update extends TradeshiftUpdate
{
public static function getProviderId(): string
{
return 'tradeshiftBox';
}
public static function getProviderClass(): string
{
return TradeshiftBox::class;
}
public static function getProviderLabel(): string
{
return 'Tradeshift Sandbox';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2TradeshiftSandbox';
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Twitch;
use Appwrite\Auth\OAuth2\Twitch;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'twitch';
}
public static function getProviderClass(): string
{
return Twitch::class;
}
public static function getProviderLabel(): string
{
return 'Twitch';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Twitch';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_TWITCH;
}
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';
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\WordPress;
use Appwrite\Auth\OAuth2\WordPress;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'wordpress';
}
public static function getProviderClass(): string
{
return WordPress::class;
}
public static function getProviderLabel(): string
{
return 'WordPress';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2WordPress';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_WORDPRESS;
}
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';
}
}
@@ -0,0 +1,65 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\X;
use Appwrite\Auth\OAuth2\X;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'x';
}
public static function getProviderClass(): string
{
return X::class;
}
public static function getProviderLabel(): string
{
return 'X';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2X';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_X;
}
public static function getClientIdParamName(): string
{
return 'customerKey';
}
public static function getClientSecretParamName(): string
{
return 'secretKey';
}
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';
}
}
@@ -0,0 +1,74 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Document;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
class XList extends Action
{
use HTTP;
public static function getName()
{
return 'listProjectOAuth2';
}
public function __construct()
{
$this
->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: <<<EOT
Get a list of all OAuth2 providers supported by the server, along with the project's configuration for each. Credential fields are write-only and always returned empty.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_OAUTH2_PROVIDER_LIST,
)
]
))
->inject('response')
->inject('project')
->callback($this->action(...));
}
public function action(
Response $response,
Document $project,
): void {
$providers = Config::getParam('oAuthProviders', []);
$actions = Base::getProviderActions();
$documents = [];
foreach ($actions as $providerId => $updateClass) {
if (!($providers[$providerId]['enabled'] ?? false)) {
// Disabled by Appwrite configuration, exclude from response
continue;
}
$action = new $updateClass();
$documents[] = $action->buildReadResponse($project);
}
$response->dynamic(new Document([
'total' => \count($documents),
'providers' => $documents,
]), Response::MODEL_OAUTH2_PROVIDER_LIST);
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Yahoo;
use Appwrite\Auth\OAuth2\Yahoo;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'yahoo';
}
public static function getProviderClass(): string
{
return Yahoo::class;
}
public static function getProviderLabel(): string
{
return 'Yahoo';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Yahoo';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_YAHOO;
}
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';
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Yandex;
use Appwrite\Auth\OAuth2\Yandex;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'yandex';
}
public static function getProviderClass(): string
{
return Yandex::class;
}
public static function getProviderLabel(): string
{
return 'Yandex';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Yandex';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_YANDEX;
}
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';
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Zoho;
use Appwrite\Auth\OAuth2\Zoho;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'zoho';
}
public static function getProviderClass(): string
{
return Zoho::class;
}
public static function getProviderLabel(): string
{
return 'Zoho';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Zoho';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_ZOHO;
}
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';
}
}
@@ -0,0 +1,55 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Zoom;
use Appwrite\Auth\OAuth2\Zoom;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Base;
use Appwrite\Utopia\Response;
class Update extends Base
{
public static function getProviderId(): string
{
return 'zoom';
}
public static function getProviderClass(): string
{
return Zoom::class;
}
public static function getProviderLabel(): string
{
return 'Zoom';
}
public static function getProviderSDKMethod(): string
{
return 'updateOAuth2Zoom';
}
public static function getResponseModel(): string
{
return Response::MODEL_OAUTH2_ZOOM;
}
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';
}
}
@@ -16,6 +16,50 @@ 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\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;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Bitbucket\Update as UpdateOAuth2Bitbucket;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Bitly\Update as UpdateOAuth2Bitly;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Box\Update as UpdateOAuth2Box;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Dailymotion\Update as UpdateOAuth2Dailymotion;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Discord\Update as UpdateOAuth2Discord;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Disqus\Update as UpdateOAuth2Disqus;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Dropbox\Update as UpdateOAuth2Dropbox;
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;
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;
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;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Salesforce\Update as UpdateOAuth2Salesforce;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Slack\Update as UpdateOAuth2Slack;
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\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;
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;
use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\Zoom\Update as UpdateOAuth2Zoom;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Create as CreateAndroidPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Update as UpdateAndroidPlatform;
use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple\Create as CreateApplePlatform;
@@ -129,5 +173,51 @@ class Http extends Service
// Auth Methods
$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());
$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());
$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());
$this->addAction(UpdateOAuth2Tradeshift::getName(), new UpdateOAuth2Tradeshift());
$this->addAction(UpdateOAuth2TradeshiftSandbox::getName(), new UpdateOAuth2TradeshiftSandbox());
$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());
$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());
$this->addAction(UpdateOAuth2Apple::getName(), new UpdateOAuth2Apple());
$this->addAction(UpdateOAuth2Microsoft::getName(), new UpdateOAuth2Microsoft());
}
}
@@ -391,6 +391,8 @@ class Migrations extends Action
'keys.write',
'platforms.read',
'platforms.write',
'oauth2.read',
'oauth2.write',
'mocks.read',
'mocks.write',
'policies.read',
+44
View File
@@ -278,6 +278,47 @@ 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';
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';
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';
public const MODEL_OAUTH2_TRADESHIFT = 'oAuth2Tradeshift';
public const MODEL_OAUTH2_PAYPAL = 'oAuth2Paypal';
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_KEYCLOAK = 'oAuth2Keycloak';
public const MODEL_OAUTH2_OIDC = 'oAuth2Oidc';
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
public const MODEL_HEALTH_STATUS = 'healthStatus';
@@ -290,6 +331,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';
@@ -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,
@@ -0,0 +1,37 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class ConsoleOAuth2Provider extends Model
{
public function __construct()
{
$this
->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;
}
}
@@ -0,0 +1,37 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class ConsoleOAuth2ProviderList extends Model
{
public function __construct()
{
$this
->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;
}
}
@@ -0,0 +1,49 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
use Appwrite\Utopia\Response\Model;
class ConsoleOAuth2ProviderParameter extends Model
{
public function __construct()
{
$this
->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;
}
}
@@ -0,0 +1,47 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Amazon extends OAuth2Base
{
public array $conditions = [
'$id' => 'amazon',
];
public function getProviderLabel(): string
{
return 'Amazon';
}
public function getClientIdExample(): string
{
return 'amzn1.application-oa2-client.87400c00000000000000000000063d5b2';
}
public function getClientSecretExample(): string
{
return '79ffe4000000000000000000000000000000000000000000000000000002de55';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Amazon';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_AMAZON;
}
}
@@ -0,0 +1,103 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Apple extends OAuth2Base
{
public array $conditions = [
'$id' => 'apple',
];
public function getProviderLabel(): string
{
return 'Apple';
}
public function getClientIdExample(): string
{
return 'ip.appwrite.app.web';
}
public function getClientSecretExample(): string
{
// Unused: this model overrides __construct() to expose keyId, teamId
// and p8File instead of a single clientSecret field.
return '';
}
public function getClientIdFieldName(): string
{
return 'serviceId';
}
public function getClientIdLabel(): string
{
return 'service ID';
}
public function __construct()
{
// 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('$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.',
'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 OAuth2 key ID.',
'default' => '',
'example' => 'P4000000N8',
])
->addRule('teamId', [
'type' => self::TYPE_STRING,
'description' => 'Apple OAuth2 team ID.',
'default' => '',
'example' => 'D4000000R6',
])
->addRule('p8File', [
'type' => self::TYPE_STRING,
'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-----',
]);
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Apple';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_APPLE;
}
}
@@ -0,0 +1,59 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Auth0 extends OAuth2Base
{
public array $conditions = [
'$id' => 'auth0',
];
public function getProviderLabel(): string
{
return 'Auth0';
}
public function getClientIdExample(): string
{
return 'OaOkIA000000000000000000005KLSYq';
}
public function getClientSecretExample(): string
{
return 'zXz0000-00000000000000000000000000000-00000000000000000000PJafnF';
}
public function __construct()
{
parent::__construct();
$this->addRule('endpoint', [
'type' => self::TYPE_STRING,
'description' => 'Auth0 OAuth2 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;
}
}
@@ -0,0 +1,59 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Authentik extends OAuth2Base
{
public array $conditions = [
'$id' => 'authentik',
];
public function getProviderLabel(): string
{
return 'Authentik';
}
public function getClientIdExample(): string
{
return 'dTKOPa0000000000000000000000000000e7G8hv';
}
public function getClientSecretExample(): string
{
return 'ntQadq000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000Hp5WK';
}
public function __construct()
{
parent::__construct();
$this->addRule('endpoint', [
'type' => self::TYPE_STRING,
'description' => 'Authentik OAuth2 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;
}
}
@@ -0,0 +1,47 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Autodesk extends OAuth2Base
{
public array $conditions = [
'$id' => 'autodesk',
];
public function getProviderLabel(): string
{
return 'Autodesk';
}
public function getClientIdExample(): string
{
return '5zw90v00000000000000000000kVYXN7';
}
public function getClientSecretExample(): string
{
return '7I000000000000MW';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Autodesk';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_AUTODESK;
}
}
@@ -0,0 +1,125 @@
<?php
namespace Appwrite\Utopia\Response\Model;
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() . ' OAuth2 ' . $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() . ' OAuth2 ' . $this->getClientSecretLabel() . '.';
}
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.',
'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(),
]);
}
}
@@ -0,0 +1,67 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Bitbucket extends OAuth2Base
{
public array $conditions = [
'$id' => 'bitbucket',
];
public function getProviderLabel(): string
{
return 'Bitbucket';
}
public function getClientIdExample(): string
{
return 'Knt70000000000ByRc';
}
public function getClientSecretExample(): string
{
return 'NMfLZJ00000000000000000000TLQdDx';
}
public function getClientIdFieldName(): string
{
return 'key';
}
public function getClientSecretFieldName(): string
{
return 'secret';
}
public function getClientIdLabel(): string
{
return 'key';
}
public function getClientSecretLabel(): string
{
return 'secret';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Bitbucket';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_BITBUCKET;
}
}
@@ -0,0 +1,47 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Bitly extends OAuth2Base
{
public array $conditions = [
'$id' => 'bitly',
];
public function getProviderLabel(): string
{
return 'Bitly';
}
public function getClientIdExample(): string
{
return 'd95151000000000000000000000000000067af9b';
}
public function getClientSecretExample(): string
{
return 'a13e250000000000000000000000000000d73095';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Bitly';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_BITLY;
}
}
@@ -0,0 +1,47 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Box extends OAuth2Base
{
public array $conditions = [
'$id' => 'box',
];
public function getProviderLabel(): string
{
return 'Box';
}
public function getClientIdExample(): string
{
return 'deglcs00000000000000000000x2og6y';
}
public function getClientSecretExample(): string
{
return 'OKM1f100000000000000000000eshEif';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Box';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_BOX;
}
}
@@ -0,0 +1,67 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Dailymotion extends OAuth2Base
{
public array $conditions = [
'$id' => 'dailymotion',
];
public function getProviderLabel(): string
{
return 'Dailymotion';
}
public function getClientIdExample(): string
{
return '07a9000000000000067f';
}
public function getClientSecretExample(): string
{
return 'a399a90000000000000000000000000000d90639';
}
public function getClientIdFieldName(): string
{
return 'apiKey';
}
public function getClientSecretFieldName(): string
{
return 'apiSecret';
}
public function getClientIdLabel(): string
{
return 'API key';
}
public function getClientSecretLabel(): string
{
return 'API secret';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Dailymotion';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_DAILYMOTION;
}
}
@@ -0,0 +1,47 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Discord extends OAuth2Base
{
public array $conditions = [
'$id' => 'discord',
];
public function getProviderLabel(): string
{
return 'Discord';
}
public function getClientIdExample(): string
{
return '950722000000343754';
}
public function getClientSecretExample(): string
{
return 'YmPXnM000000000000000000002zFg5D';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Discord';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_DISCORD;
}
}
@@ -0,0 +1,67 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Disqus extends OAuth2Base
{
public array $conditions = [
'$id' => 'disqus',
];
public function getProviderLabel(): string
{
return 'Disqus';
}
public function getClientIdExample(): string
{
return 'cgegH70000000000000000000000000000000000000000000000000000Hr1nYX';
}
public function getClientSecretExample(): string
{
return 'W7Bykj00000000000000000000000000000000000000000000000000003o43w9';
}
public function getClientIdFieldName(): string
{
return 'publicKey';
}
public function getClientSecretFieldName(): string
{
return 'secretKey';
}
public function getClientIdLabel(): string
{
return 'public key';
}
public function getClientSecretLabel(): string
{
return 'secret key';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Disqus';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_DISQUS;
}
}
@@ -0,0 +1,67 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Dropbox extends OAuth2Base
{
public array $conditions = [
'$id' => 'dropbox',
];
public function getProviderLabel(): string
{
return 'Dropbox';
}
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';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Dropbox';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_DROPBOX;
}
}
@@ -0,0 +1,67 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Etsy extends OAuth2Base
{
public array $conditions = [
'$id' => 'etsy',
];
public function getProviderLabel(): string
{
return 'Etsy';
}
public function getClientIdExample(): string
{
return 'nsgzxh0000000000008j85a2';
}
public function getClientSecretExample(): string
{
return 'tp000000ru';
}
public function getClientIdFieldName(): string
{
return 'keyString';
}
public function getClientSecretFieldName(): string
{
return 'sharedSecret';
}
public function getClientIdLabel(): string
{
return 'keystring';
}
public function getClientSecretLabel(): string
{
return 'shared secret';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Etsy';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_ETSY;
}
}
@@ -0,0 +1,67 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Facebook extends OAuth2Base
{
public array $conditions = [
'$id' => 'facebook',
];
public function getProviderLabel(): string
{
return 'Facebook';
}
public function getClientIdExample(): string
{
return '260600000007694';
}
public function getClientSecretExample(): string
{
return '2d0b2800000000000000000000d38af4';
}
public function getClientIdFieldName(): string
{
return 'appId';
}
public function getClientSecretFieldName(): string
{
return 'appSecret';
}
public function getClientIdLabel(): string
{
return 'app ID';
}
public function getClientSecretLabel(): string
{
return 'app secret';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Facebook';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_FACEBOOK;
}
}
@@ -0,0 +1,47 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Figma extends OAuth2Base
{
public array $conditions = [
'$id' => 'figma',
];
public function getProviderLabel(): string
{
return 'Figma';
}
public function getClientIdExample(): string
{
return 'byay5H0000000000VtiI40';
}
public function getClientSecretExample(): string
{
return 'yEpOYn0000000000000000004iIsU5';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Figma';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_FIGMA;
}
}
@@ -0,0 +1,59 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2FusionAuth extends OAuth2Base
{
public array $conditions = [
'$id' => '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;
}
}
@@ -0,0 +1,52 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2GitHub extends OAuth2Base
{
public array $conditions = [
'$id' => 'github',
];
public function getProviderLabel(): string
{
return 'GitHub';
}
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.';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2GitHub';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_GITHUB;
}
}
@@ -0,0 +1,79 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Gitlab extends OAuth2Base
{
public array $conditions = [
'$id' => 'gitlab',
];
public function getProviderLabel(): string
{
return 'GitLab';
}
public function getClientIdExample(): string
{
return 'd41ffe0000000000000000000000000000000000000000000000000000d5e252';
}
public function getClientSecretExample(): string
{
return 'gloas-838cfa0000000000000000000000000000000000000000000000000000ecbb38';
}
public function getClientIdFieldName(): string
{
return 'applicationId';
}
public function getClientSecretFieldName(): string
{
return 'secret';
}
public function getClientIdLabel(): string
{
return 'application ID';
}
public function getClientSecretLabel(): string
{
return 'secret';
}
public function __construct()
{
parent::__construct();
$this->addRule('endpoint', [
'type' => self::TYPE_STRING,
'description' => 'GitLab OAuth2 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;
}
}
@@ -0,0 +1,47 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Google extends OAuth2Base
{
public array $conditions = [
'$id' => 'google',
];
public function getProviderLabel(): string
{
return 'Google';
}
public function getClientIdExample(): string
{
return '120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com';
}
public function getClientSecretExample(): string
{
return 'GOCSPX-2k8gsR0000000000000000VNahJj';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Google';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_GOOGLE;
}
}
@@ -0,0 +1,66 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Keycloak extends OAuth2Base
{
public array $conditions = [
'$id' => '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;
}
}
@@ -0,0 +1,47 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Kick extends OAuth2Base
{
public array $conditions = [
'$id' => 'kick',
];
public function getProviderLabel(): string
{
return 'Kick';
}
public function getClientIdExample(): string
{
return '01KQ7C00000000000001MFHS32';
}
public function getClientSecretExample(): string
{
return '34ac5600000000000000000000000000000000000000000000000000e830c8b';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Kick';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_KICK;
}
}
@@ -0,0 +1,57 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Linkedin extends OAuth2Base
{
public array $conditions = [
'$id' => 'linkedin',
];
public function getProviderLabel(): string
{
return 'LinkedIn';
}
public function getClientIdExample(): string
{
return '770000000000dv';
}
public function getClientSecretExample(): string
{
return 'WPL_AP1.2Bf0000000000000./HtlYw==';
}
public function getClientSecretFieldName(): string
{
return 'primaryClientSecret';
}
public function getClientSecretLabel(): string
{
return 'primary client secret';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Linkedin';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_LINKEDIN;
}
}
@@ -0,0 +1,79 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Microsoft extends OAuth2Base
{
public array $conditions = [
'$id' => 'microsoft',
];
public function getProviderLabel(): string
{
return 'Microsoft';
}
public function getClientIdExample(): string
{
return '00001111-aaaa-2222-bbbb-3333cccc4444';
}
public function getClientSecretExample(): string
{
return 'A1bC2dE3fH4iJ5kL6mN7oP8qR9sT0u';
}
public function getClientIdFieldName(): string
{
return 'applicationId';
}
public function getClientSecretFieldName(): string
{
return 'applicationSecret';
}
public function getClientIdLabel(): string
{
return 'application ID';
}
public function getClientSecretLabel(): string
{
return 'application secret';
}
public function __construct()
{
parent::__construct();
$this->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;
}
}
@@ -0,0 +1,57 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Notion extends OAuth2Base
{
public array $conditions = [
'$id' => 'notion',
];
public function getProviderLabel(): string
{
return 'Notion';
}
public function getClientIdExample(): string
{
return '341d8700-0000-0000-0000-000000446ee3';
}
public function getClientSecretExample(): string
{
return 'secret_dLUr4b000000000000000000000000000000lFHAa9';
}
public function getClientIdFieldName(): string
{
return 'oauthClientId';
}
public function getClientSecretFieldName(): string
{
return 'oauthClientSecret';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Notion';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_NOTION;
}
}
@@ -0,0 +1,78 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Oidc extends OAuth2Base
{
public array $conditions = [
'$id' => 'oidc',
];
public function getProviderLabel(): string
{
return 'OpenID Connect';
}
public function getClientIdExample(): string
{
return 'qibI2x0000000000000000000000000006L2YFoG';
}
public function getClientSecretExample(): string
{
return 'Ah68ed000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003qpcHV';
}
public function __construct()
{
parent::__construct();
$this
->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;
}
}
@@ -0,0 +1,66 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Okta extends OAuth2Base
{
public array $conditions = [
'$id' => 'okta',
];
public function getProviderLabel(): string
{
return 'Okta';
}
public function getClientIdExample(): string
{
return '0oa00000000000000698';
}
public function getClientSecretExample(): string
{
return 'Kiq0000000000000000000000000000000000000-00000000000H2L5-3SJ-vRV';
}
public function __construct()
{
parent::__construct();
$this->addRule('domain', [
'type' => self::TYPE_STRING,
'description' => 'Okta OAuth2 domain.',
'default' => '',
'example' => 'trial-6400025.okta.com',
]);
$this->addRule('authorizationServerId', [
'type' => self::TYPE_STRING,
'description' => 'Okta OAuth2 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;
}
}
@@ -0,0 +1,57 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Paypal extends OAuth2Base
{
public array $conditions = [
'$id' => ['paypal', 'paypalSandbox'],
];
public function getProviderLabel(): string
{
return 'PayPal';
}
public function getClientIdExample(): string
{
return 'AdhIEG7-000000000000-0000000000000000000000000000000-0000000000000000000000-2pyB';
}
public function getClientSecretExample(): string
{
return 'EH8KCXtew--000000000000000000000000000000000000000_C-1_5UP_000000000000000CB7KDp';
}
public function getClientSecretFieldName(): string
{
return 'secretKey';
}
public function getClientSecretLabel(): string
{
return 'secret key';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Paypal';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_PAYPAL;
}
}
@@ -0,0 +1,47 @@
<?php
namespace Appwrite\Utopia\Response\Model;
use Appwrite\Utopia\Response;
class OAuth2Podio extends OAuth2Base
{
public array $conditions = [
'$id' => 'podio',
];
public function getProviderLabel(): string
{
return 'Podio';
}
public function getClientIdExample(): string
{
return 'appwrite-oauth-test-app';
}
public function getClientSecretExample(): string
{
return 'Rn247T0000000000000000000000000000000000000000000000000000W2zWTN';
}
/**
* Get Name
*
* @return string
*/
public function getName(): string
{
return 'OAuth2Podio';
}
/**
* Get Type
*
* @return string
*/
public function getType(): string
{
return Response::MODEL_OAUTH2_PODIO;
}
}

Some files were not shown because too many files have changed in this diff Show More