mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
remove: jwt logic. serve the jwt directly from the model itself.
This commit is contained in:
@@ -499,6 +499,11 @@ return [
|
||||
'description' => 'The requested file token has expired.',
|
||||
'code' => 401,
|
||||
],
|
||||
Exception::TOKEN_RESOURCE_INVALID => [
|
||||
'name' => Exception::TOKEN_RESOURCE_INVALID,
|
||||
'description' => 'The resource for the token is invalid.',
|
||||
'code' => 400,
|
||||
],
|
||||
|
||||
/** VCS */
|
||||
Exception::INSTALLATION_NOT_FOUND => [
|
||||
|
||||
+33
-18
@@ -925,31 +925,46 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
|
||||
|
||||
$token = Authorization::skip(fn () => $dbForProject->getDocument('resourceTokens', $tokenId));
|
||||
|
||||
if ($token->isEmpty() || $token->getAttribute('secret') !== $secret) {
|
||||
if ($token->isEmpty()) {
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
if ($token->getAttribute('resourceType') === TOKENS_RESOURCE_TYPE_FILES) {
|
||||
$internalIds = explode(':', $token->getAttribute('resourceInternalId'));
|
||||
$ids = explode(':', $token->getAttribute('resourceId'));
|
||||
$expiry = $token->getAttribute('expire');
|
||||
|
||||
if (count($internalIds) !== 2 || count($ids) !== 2) {
|
||||
if ($expiry !== null) {
|
||||
$now = new \DateTime();
|
||||
$expiryDate = new \DateTime($expiry);
|
||||
|
||||
if ($expiryDate < $now) {
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
$accessedAt = $token->getAttribute('accessedAt', 0);
|
||||
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), - APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) {
|
||||
$token->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token));
|
||||
}
|
||||
|
||||
return new Document([
|
||||
'bucketId' => $ids[0],
|
||||
'fileId' => $ids[1],
|
||||
'bucketInternalId' => $internalIds[0],
|
||||
'fileInternalId' => $internalIds[1],
|
||||
]);
|
||||
}
|
||||
|
||||
return match ($token->getAttribute('resourceType')) {
|
||||
TOKENS_RESOURCE_TYPE_FILES => (function () use ($token, $dbForProject) {
|
||||
$internalIds = explode(':', $token->getAttribute('resourceInternalId'));
|
||||
$ids = explode(':', $token->getAttribute('resourceId'));
|
||||
|
||||
if (count($internalIds) !== 2 || count($ids) !== 2) {
|
||||
return new Document([]);
|
||||
}
|
||||
|
||||
$accessedAt = $token->getAttribute('accessedAt', 0);
|
||||
if (empty($accessedAt) || DatabaseDateTime::formatTz(DatabaseDateTime::addSeconds(new \DateTime(), - APP_RESOURCE_TOKEN_ACCESS)) > $accessedAt) {
|
||||
$token->setAttribute('accessedAt', DatabaseDateTime::now());
|
||||
Authorization::skip(fn () => $dbForProject->updateDocument('resourceTokens', $token->getId(), $token));
|
||||
}
|
||||
|
||||
return new Document([
|
||||
'bucketId' => $ids[0],
|
||||
'fileId' => $ids[1],
|
||||
'bucketInternalId' => $internalIds[0],
|
||||
'fileInternalId' => $internalIds[1],
|
||||
]);
|
||||
})(),
|
||||
|
||||
default => throw new Exception(Exception::TOKEN_RESOURCE_INVALID),
|
||||
};
|
||||
}
|
||||
return new Document([]);
|
||||
}, ['project', 'dbForProject', 'request']);
|
||||
|
||||
@@ -322,7 +322,7 @@ class Exception extends \Exception
|
||||
/** Tokens */
|
||||
public const TOKEN_NOT_FOUND = 'token_not_found';
|
||||
public const TOKEN_EXPIRED = 'token_expired';
|
||||
|
||||
public const TOKEN_RESOURCE_INVALID = 'token_resource_invalid';
|
||||
|
||||
protected string $type = '';
|
||||
protected array $errors = [];
|
||||
|
||||
@@ -71,7 +71,7 @@ class Create extends Action
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $bucketId, string $fileId, ?string $expire, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents)
|
||||
public function action(string $bucketId, string $fileId, ?string $expire, ?array $permissions, Response $response, Database $dbForProject, Document $user, Event $queueForEvents): void
|
||||
{
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Tokens\Http\Tokens\JWT;
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\SDK\AuthType;
|
||||
use Appwrite\SDK\ContentType;
|
||||
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\UID;
|
||||
use Utopia\Platform\Action;
|
||||
use Utopia\Platform\Scope\HTTP;
|
||||
use Utopia\System\System;
|
||||
|
||||
class Get extends Action
|
||||
{
|
||||
use HTTP;
|
||||
|
||||
public static function getName()
|
||||
{
|
||||
return 'getTokenJWT';
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET)
|
||||
->setHttpPath('/v1/tokens/:tokenId/jwt')
|
||||
->desc('Get token as JWT')
|
||||
->groups(['api', 'tokens'])
|
||||
->label('scope', 'tokens.read')
|
||||
->label('usage.metric', 'tokens.{scope}.requests.read')
|
||||
->label('usage.params', ['tokenId:{request.tokenId}'])
|
||||
->label('sdk', new Method(
|
||||
namespace: 'tokens',
|
||||
group: 'tokens',
|
||||
name: 'getJWT',
|
||||
description: <<<EOT
|
||||
Get a JWT based token by its unique ID. You can use the JWT to authenticate on behalf of the user.
|
||||
EOT,
|
||||
auth: [AuthType::SESSION, AuthType::KEY, AuthType::JWT],
|
||||
responses: [
|
||||
new SDKResponse(
|
||||
code: Response::STATUS_CODE_OK,
|
||||
model: Response::MODEL_JWT,
|
||||
)
|
||||
],
|
||||
contentType: ContentType::JSON
|
||||
))
|
||||
->param('tokenId', '', new UID(), 'File token ID.')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->callback([$this, 'action']);
|
||||
}
|
||||
|
||||
public function action(string $tokenId, Response $response, Database $dbForProject)
|
||||
{
|
||||
$token = $dbForProject->getDocument('resourceTokens', $tokenId);
|
||||
|
||||
if ($token->isEmpty()) {
|
||||
throw new Exception(Exception::TOKEN_NOT_FOUND);
|
||||
}
|
||||
|
||||
// calculate maxAge based on expiry date
|
||||
$maxAge = PHP_INT_MAX;
|
||||
$expire = $token->getAttribute('expire');
|
||||
if ($expire !== null) {
|
||||
$now = new \DateTime();
|
||||
$expiryDate = new \DateTime($expire);
|
||||
if ($expiryDate < $now) {
|
||||
throw new Exception(Exception::TOKEN_EXPIRED);
|
||||
}
|
||||
$maxAge = $expiryDate->getTimestamp() - $now->getTimestamp();
|
||||
}
|
||||
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $maxAge, 10); // Instantiate with key, algo, maxAge and leeway.
|
||||
|
||||
$response
|
||||
->setStatusCode(Response::STATUS_CODE_OK)
|
||||
->dynamic(new Document(['jwt' => $jwt->encode([
|
||||
'resourceType' => $token->getAttribute('resourceType'),
|
||||
'resourceId' => $token->getAttribute('resourceId'),
|
||||
'resourceInternalId' => $token->getAttribute('resourceInternalId'),
|
||||
'tokenId' => $token->getId(),
|
||||
'secret' => $token->getAttribute('secret')
|
||||
])]), Response::MODEL_JWT);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ use Appwrite\Platform\Modules\Tokens\Http\Tokens\Buckets\Files\Create as CreateF
|
||||
use Appwrite\Platform\Modules\Tokens\Http\Tokens\Buckets\Files\XList as ListFileTokens;
|
||||
use Appwrite\Platform\Modules\Tokens\Http\Tokens\Delete as DeleteToken;
|
||||
use Appwrite\Platform\Modules\Tokens\Http\Tokens\Get as GetToken;
|
||||
use Appwrite\Platform\Modules\Tokens\Http\Tokens\JWT\Get as GetTokenJWT;
|
||||
use Appwrite\Platform\Modules\Tokens\Http\Tokens\Update as UpdateToken;
|
||||
use Utopia\Platform\Service;
|
||||
|
||||
@@ -18,7 +17,6 @@ class Http extends Service
|
||||
$this
|
||||
->addAction(CreateFileToken::getName(), new CreateFileToken())
|
||||
->addAction(GetToken::getName(), new GetToken())
|
||||
->addAction(GetTokenJWT::getName(), new GetTokenJWT())
|
||||
->addAction(ListFileTokens::getName(), new ListFileTokens())
|
||||
->addAction(UpdateToken::getName(), new UpdateToken())
|
||||
->addAction(DeleteToken::getName(), new DeleteToken())
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace Appwrite\Utopia\Response\Model;
|
||||
|
||||
use Ahc\Jwt\JWT;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Appwrite\Utopia\Response\Model;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\System\System;
|
||||
|
||||
class ResourceToken extends Model
|
||||
{
|
||||
@@ -47,6 +50,13 @@ class ResourceToken extends Model
|
||||
'default' => '',
|
||||
'example' => self::TYPE_DATETIME_EXAMPLE,
|
||||
])
|
||||
->addRule('secret', [
|
||||
'type' => self::TYPE_STRING,
|
||||
'description' => 'JWT encoded string.',
|
||||
'default' => '',
|
||||
// this is a secret but is converted to a JWT token when sent back to the client after filter.
|
||||
'example' => 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c',
|
||||
])
|
||||
->addRule('accessedAt', [
|
||||
'type' => self::TYPE_DATETIME,
|
||||
'description' => 'Most recent access date in ISO 8601 format. This attribute is only updated again after ' . APP_RESOURCE_TOKEN_ACCESS / 60 / 60 . ' hours.',
|
||||
@@ -56,6 +66,32 @@ class ResourceToken extends Model
|
||||
;
|
||||
}
|
||||
|
||||
public function filter(Document $document): Document
|
||||
{
|
||||
$maxAge = PHP_INT_MAX;
|
||||
$expire = $document->getAttribute('expire');
|
||||
|
||||
if ($expire !== null) {
|
||||
$now = new \DateTime();
|
||||
$expiryDate = new \DateTime($expire);
|
||||
|
||||
// set 1 min if expired, we check for expiry later on route hooks for validation!
|
||||
$maxAge = min(360, $expiryDate->getTimestamp() - $now->getTimestamp());
|
||||
}
|
||||
|
||||
$jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $maxAge, 10);
|
||||
$secret = $jwt->encode([
|
||||
'tokenId' => $document->getId(),
|
||||
'resourceId' => $document->getAttribute('resourceId'),
|
||||
'resourceType' => $document->getAttribute('resourceType'),
|
||||
'resourceInternalId' => $document->getAttribute('resourceInternalId'),
|
||||
]);
|
||||
|
||||
$document->setAttribute('secret', $secret);
|
||||
|
||||
return $document;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name
|
||||
*
|
||||
|
||||
@@ -66,7 +66,7 @@ trait TokensBase
|
||||
return [
|
||||
'fileId' => $fileId,
|
||||
'bucketId' => $bucketId,
|
||||
'tokenId' => $token['body']['$id'],
|
||||
'token' => $token['body'],
|
||||
'guestHeaders' => [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
@@ -143,22 +143,12 @@ trait TokensBase
|
||||
*/
|
||||
public function testPreviewFileWithToken(array $data): array
|
||||
{
|
||||
$token = $data['token'];
|
||||
$fileId = $data['fileId'];
|
||||
$tokenId = $data['tokenId'];
|
||||
$bucketId = $data['bucketId'];
|
||||
$guestHeaders = $data['guestHeaders'];
|
||||
$adminHeaders = array_merge($guestHeaders, ['x-appwrite-key' => $this->getProject()['apiKey']]);
|
||||
|
||||
// Generate JWT as an admin user.
|
||||
$tokenJWT = $this->client->call(
|
||||
Client::METHOD_GET,
|
||||
'/tokens/' . $tokenId . '/jwt/',
|
||||
$adminHeaders
|
||||
);
|
||||
$this->assertEquals(200, $tokenJWT['headers']['status-code']);
|
||||
$this->assertArrayHasKey('jwt', $tokenJWT['body']);
|
||||
|
||||
$tokenJWT = $tokenJWT['body']['jwt'];
|
||||
$tokenJWT = $token['secret'];
|
||||
|
||||
// Generate a preview
|
||||
$filePreview = $this->client->call(
|
||||
|
||||
Reference in New Issue
Block a user