Re-introduce project JWT endpoint

This commit is contained in:
Matej Bačo
2026-04-28 15:31:10 +02:00
parent 3d3f5934c6
commit 8f176166c9
2 changed files with 96 additions and 0 deletions
+44
View File
@@ -1,5 +1,6 @@
<?php
use Ahc\Jwt\JWT;
use Appwrite\Auth\Validator\MockNumber;
use Appwrite\Extend\Exception;
use Appwrite\SDK\AuthType;
@@ -15,6 +16,7 @@ use Utopia\System\System;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean;
use Utopia\Validator\Nullable;
use Utopia\Validator\Range;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
@@ -58,6 +60,48 @@ Http::get('/v1/projects/:projectId')
$response->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')
@@ -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