GitHub oauth endpoint

This commit is contained in:
Matej Bačo
2026-04-24 11:17:18 +02:00
parent 7fbfb6266b
commit 93f7a0d902
9 changed files with 201 additions and 0 deletions
+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",
],
];
+7
View File
@@ -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
+30
View File
@@ -1,6 +1,7 @@
<?php
namespace Appwrite\Auth\OAuth2;
use Utopia\Fetch\Client as FetchClient;
use Appwrite\Auth\OAuth2;
@@ -219,4 +220,33 @@ 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
}
}
@@ -0,0 +1,144 @@
<?php
namespace Appwrite\Platform\Modules\Project\Http\Project\OAuth2\GitHub;
use Appwrite\Auth\OAuth2\Github;
use Appwrite\Extend\Exception;
use Appwrite\Platform\Action;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization;
use Utopia\Platform\Scope\HTTP;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Text;
class Update extends Action
{
use HTTP;
public static function getName()
{
return 'updateProjectOAuth2GitHub';
}
public static function getProviderId(): string
{
return 'github';
}
/**
* @return class-string
*/
public static function getProviderClass(): string
{
return Github::class;
}
public function __construct()
{
$this
->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH)
->setHttpPath('/v1/project/oauth2/github')
->desc('Update project OAuth2 GitHub')
->groups(['api', 'project'])
->label('scope', 'oauth2.write')
->label('event', 'oauth2.github.update')
->label('audits.event', 'project.oauth2.github.update')
->label('audits.resource', 'project.oauth2/{response.$id}')
->label('sdk', new Method(
namespace: 'project',
group: 'oauth2',
name: 'updateOAuth2GitHub',
description: <<<EOT
Update the project OAuth2 GitHub configuration.
EOT,
auth: [AuthType::ADMIN, AuthType::KEY],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_OAUTH2_GITHUB,
)
],
))
->param('clientId', null, new Nullable(new Text(256, 0)), 'Client ID of GitHub OAuth2 app, or App ID of GitHub generic app. For example: e4d87900000000540733', optional: true)
->param('clientSecret', null, new Nullable(new Text(512, 0)), 'Client secret of GitHub OAuth2 app, or GitHub generic app. For example: 5e07c00000000000000000000000000000198bcc', optional: true)
->param('enabled', null, new Nullable(new Boolean()), 'OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid.', true)
->inject('response')
->inject('dbForPlatform')
->inject('project')
->inject('authorization')
->callback($this->action(...));
}
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);
}
}
@@ -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());
}
}
@@ -391,6 +391,8 @@ class Migrations extends Action
'keys.write',
'platforms.read',
'platforms.write',
'oauth2.read',
'oauth2.write',
'mocks.read',
'mocks.write',
'policies.read',
+2
View File
@@ -90,6 +90,8 @@ const API_SCOPES = [
'tokens.write',
'platforms.read',
'platforms.write',
'oauth2.read',
'oauth2.write',
];
const BASE_PERMISSIONS = [
+2
View File
@@ -169,6 +169,8 @@ trait ProjectCustom
'keys.write',
'platforms.read',
'platforms.write',
'oauth2.read',
'oauth2.write',
'mocks.read',
'mocks.write',
'policies.read',