From 8f176166c9f1e24eb7d7bf2124e2f725bbcf5b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 Apr 2026 15:31:10 +0200 Subject: [PATCH] Re-introduce project JWT endpoint --- app/controllers/api/projects.php | 44 ++++++++++++++++ .../Projects/ProjectsConsoleClientTest.php | 52 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 494aa11150..da772d6dbb 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -1,5 +1,6 @@ dynamic($project, Response::MODEL_PROJECT); }); +// JWT Keys + +Http::post('/v1/projects/:projectId/jwts') + ->groups(['api', 'projects']) + ->desc('Create JWT') + ->label('scope', 'projects.write') + ->label('sdk', new Method( + namespace: 'projects', + group: 'auth', + name: 'createJWT', + description: '/docs/references/projects/create-jwt.md', + auth: [AuthType::ADMIN], + responses: [ + new SDKResponse( + code: Response::STATUS_CODE_CREATED, + model: Response::MODEL_JWT, + ) + ] + )) + ->param('projectId', '', fn (Database $dbForPlatform) => new UID($dbForPlatform->getAdapter()->getMaxUIDLength()), 'Project unique ID.', false, ['dbForPlatform']) + ->param('scopes', [], new ArrayList(new WhiteList(array_keys(Config::getParam('projectScopes')), true), APP_LIMIT_ARRAY_PARAMS_SIZE), 'List of scopes allowed for JWT key. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed.') + ->param('duration', 900, new Range(0, 3600), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.', true) + ->inject('response') + ->inject('dbForPlatform') + ->action(function (string $projectId, array $scopes, int $duration, Response $response, Database $dbForPlatform) { + + $project = $dbForPlatform->getDocument('projects', $projectId); + + if ($project->isEmpty()) { + throw new Exception(Exception::PROJECT_NOT_FOUND); + } + + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 0); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic(new Document(['jwt' => API_KEY_DYNAMIC . '_' . $jwt->encode([ + 'projectId' => $project->getId(), + 'scopes' => $scopes + ])]), Response::MODEL_JWT); + }); + // Backwards compatibility Http::patch('/v1/projects/:projectId/oauth2') ->desc('Update project OAuth2') diff --git a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php index 8322e37de1..d71537d534 100644 --- a/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php +++ b/tests/e2e/Services/Projects/ProjectsConsoleClientTest.php @@ -3941,6 +3941,58 @@ class ProjectsConsoleClientTest extends Scope $this->assertEmpty($response['body']); } + // JWT Keys + + public function testJWTKey(): void + { + $data = $this->setupProjectData(); + $id = $data['projectId']; + + // Create JWT key + $response = $this->client->call(Client::METHOD_POST, '/projects/' . $id . '/jwts', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'duration' => 5, + 'scopes' => ['users.read'], + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['jwt']); + + $jwt = $response['body']['jwt']; + + // Ensure JWT key works + $response = $this->client->call(Client::METHOD_GET, '/users', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $id, + 'x-appwrite-key' => $jwt, + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertArrayHasKey('users', $response['body']); + + // Ensure JWT key respect scopes + $response = $this->client->call(Client::METHOD_GET, '/functions', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $id, + 'x-appwrite-key' => $jwt, + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + + // Ensure JWT key expires + \sleep(10); + + $response = $this->client->call(Client::METHOD_GET, '/users', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $id, + 'x-appwrite-key' => $jwt, + ]); + + $this->assertEquals(401, $response['headers']['status-code']); + } + // Platforms public function testCreateProjectPlatform(): void