diff --git a/app/config/roles.php b/app/config/roles.php index 33c7ffc9de..d653b4857c 100644 --- a/app/config/roles.php +++ b/app/config/roles.php @@ -55,6 +55,8 @@ $admins = [ 'tables.write', 'platforms.read', 'platforms.write', + 'oauth2.read', + 'oauth2.write', 'mocks.read', 'mocks.write', 'policies.read', diff --git a/app/config/scopes/project.php b/app/config/scopes/project.php index 592e032ba1..947cd863f8 100644 --- a/app/config/scopes/project.php +++ b/app/config/scopes/project.php @@ -228,4 +228,12 @@ return [ // List of publicly visible scopes "description" => "Access to create, update, and delete project\'s templates", ], + "oauth2.read" => [ + "description" => + "Access to read project\'s OAuth2 configuration", + ], + "oauth2.write" => [ + "description" => + "Access to update project\'s OAuth2 configuration", + ], ]; diff --git a/src/Appwrite/Auth/OAuth2.php b/src/Appwrite/Auth/OAuth2.php index a8a2d175b5..3861004498 100644 --- a/src/Appwrite/Auth/OAuth2.php +++ b/src/Appwrite/Auth/OAuth2.php @@ -50,6 +50,13 @@ abstract class OAuth2 $this->addScope($scope); } } + + /** + * Check if the OAuth credentials are valid + * + * @throws \Exception + */ + abstract public function verifyCredentials(): void; /** * @return string diff --git a/src/Appwrite/Auth/OAuth2/Github.php b/src/Appwrite/Auth/OAuth2/Github.php index 1cefc397c5..49d62aa022 100644 --- a/src/Appwrite/Auth/OAuth2/Github.php +++ b/src/Appwrite/Auth/OAuth2/Github.php @@ -1,6 +1,7 @@ addHeader('Accept', 'application/json'); + + $response = $client->fetch( + url: 'https://github.com/login/oauth/access_token', + method: FetchClient::METHOD_POST, + query: [ + 'client_id' => $this->appID, + 'client_secret' => $this->appSecret, + 'code' => 'intentionally-invalid-code', + 'redirect_uri' => 'intentionally-invalid-redirect', + ] + ); + + $json = \json_decode($response->getBody(), true); + + if (isset($json['error']) && $json['error'] === "Not Found") { + throw new \Exception('GitHub application with provided Client ID is does not exist.'); + } + + if (isset($json['error']) && $json['error'] === "incorrect_client_credentials") { + throw new \Exception('GitHub application with provided Client ID is valid, but the provided Client Secret is incorrect.'); + } + + // We still expect error, like redirect_uri_mismatch or bad_verification_code, + // but that indicates valid credentials + } } diff --git a/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php new file mode 100644 index 0000000000..ffdb2c78d0 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Project/Http/Project/OAuth2/GitHub/Update.php @@ -0,0 +1,144 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) + ->setHttpPath('/v1/project/oauth2/github') + ->desc('Update project OAuth2 GitHub') + ->groups(['api', 'project']) + ->label('scope', 'oauth2.write') + ->label('event', 'oauth2.github.update') + ->label('audits.event', 'project.oauth2.github.update') + ->label('audits.resource', 'project.oauth2/{response.$id}') + ->label('sdk', new Method( + namespace: 'project', + group: 'oauth2', + name: 'updateOAuth2GitHub', + description: <<param('clientId', null, new Nullable(new Text(256, 0)), 'Client ID of GitHub OAuth2 app, or App ID of GitHub generic app. For example: e4d87900000000540733', optional: true) + ->param('clientSecret', null, new Nullable(new Text(512, 0)), 'Client secret of GitHub OAuth2 app, or GitHub generic app. For example: 5e07c00000000000000000000000000000198bcc', optional: true) + ->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true) + ->inject('response') + ->inject('dbForPlatform') + ->inject('project') + ->inject('authorization') + ->callback($this->action(...)); + } + + public function action( + ?string $clientId, + ?string $clientSecret, + ?bool $enabled, + Response $response, + Database $dbForPlatform, + Document $project, + Authorization $authorization + ): void { + $providerId = self::getProviderId(); + if(!(\in_array($providerId, \array_keys(Config::getParam('oAuthProviders'))))) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Provider ' . $providerId . ' is not supported by server configuration.'); + } + + $oAuthProviders = $project->getAttribute('oAuthProviders', []); + + $appIdKey = $providerId . 'Appid'; + $appSecretKey = $providerId . 'Secret'; + $enabledKey = $providerId . 'Enabled'; + + if (!\is_null($clientId)) { + $oAuthProviders[$appIdKey] = $clientId; + } + + if (!\is_null($clientSecret)) { + $oAuthProviders[$appSecretKey] = $clientSecret; + } + + if (!\is_null($enabled)) { + $oAuthProviders[$enabledKey] = $enabled; + } + + if($enabled === true || \is_null($enabled)) { + try { + if(empty($oAuthProviders[$appIdKey]) || empty($oAuthProviders[$appSecretKey])) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Client ID and Client Secret are required when enabling OAuth2 provider.'); + } + + $providerClass = self::getProviderClass(); + $providerInstance = new $providerClass(appId: $oAuthProviders[$appIdKey], appSecret: $oAuthProviders[$appSecretKey], callback: '', state: [], scopes: []); + + $providerInstance->verifyCredentials(); + + $oAuthProviders[$enabledKey] = true; + } catch(\Throwable $err) { + if($enabled === true) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Could not enable OAuth2 provider: ' . $err->getMessage()); + } + } + } + + $updates = new Document([ + 'oAuthProviders' => $oAuthProviders + ]); + + $project = $authorization->skip(fn() => $dbForPlatform->updateDocument('projects', $project->getId(), $updates)); + + $response->dynamic(new Document([ + '$id' => $providerId, + 'enabled' => $oAuthProviders[$enabledKey] ?? false, + 'clientId' => $oAuthProviders[$appIdKey] ?? '', + 'clientSecret' => $oAuthProviders[$appSecretKey] ?? '', + ]), Response::MODEL_OAUTH2_GITHUB); + } +} diff --git a/src/Appwrite/Platform/Modules/Project/Services/Http.php b/src/Appwrite/Platform/Modules/Project/Services/Http.php index 64dad109f8..b1441be304 100644 --- a/src/Appwrite/Platform/Modules/Project/Services/Http.php +++ b/src/Appwrite/Platform/Modules/Project/Services/Http.php @@ -17,6 +17,7 @@ use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\Get as GetMockPhone use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\Update as UpdateMockPhone; use Appwrite\Platform\Modules\Project\Http\Project\MockPhone\XList as ListMockPhones; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Create as CreateAndroidPlatform; +use Appwrite\Platform\Modules\Project\Http\Project\OAuth2\GitHub\Update as UpdateOAuth2GitHub; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Android\Update as UpdateAndroidPlatform; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple\Create as CreateApplePlatform; use Appwrite\Platform\Modules\Project\Http\Project\Platforms\Apple\Update as UpdateApplePlatform; @@ -129,5 +130,8 @@ class Http extends Service // Auth Methods $this->addAction(UpdateAuthMethod::getName(), new UpdateAuthMethod()); + + // OAuth2 + $this->addAction(UpdateOAuth2GitHub::getName(), new UpdateOAuth2GitHub()); } } diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index cfe8d2d567..fa2ed5883f 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -391,6 +391,8 @@ class Migrations extends Action 'keys.write', 'platforms.read', 'platforms.write', + 'oauth2.read', + 'oauth2.write', 'mocks.read', 'mocks.write', 'policies.read', diff --git a/tests/benchmarks/http.js b/tests/benchmarks/http.js index 4009024069..6466ffd361 100644 --- a/tests/benchmarks/http.js +++ b/tests/benchmarks/http.js @@ -90,6 +90,8 @@ const API_SCOPES = [ 'tokens.write', 'platforms.read', 'platforms.write', + 'oauth2.read', + 'oauth2.write', ]; const BASE_PERMISSIONS = [ diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index f531ed774d..31d85524af 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -169,6 +169,8 @@ trait ProjectCustom 'keys.write', 'platforms.read', 'platforms.write', + 'oauth2.read', + 'oauth2.write', 'mocks.read', 'mocks.write', 'policies.read',