From eec62752ed064e2c65365e710ccfed186f396e0a Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 15 Aug 2025 16:07:37 -0700 Subject: [PATCH 1/7] fix - incorrect file token expiry --- .../Utopia/Response/Model/ResourceToken.php | 34 ++++++++++++----- .../Tokens/TokensConsoleClientTest.php | 38 +++++++++++++++++++ 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/ResourceToken.php b/src/Appwrite/Utopia/Response/Model/ResourceToken.php index ef186c3d0b..73c3efee72 100644 --- a/src/Appwrite/Utopia/Response/Model/ResourceToken.php +++ b/src/Appwrite/Utopia/Response/Model/ResourceToken.php @@ -61,24 +61,40 @@ class ResourceToken extends Model public function filter(Document $document): Document { - $maxAge = PHP_INT_MAX; $expire = $document->getAttribute('expire'); - + $now = new \DateTime(); + + // Calculate expiration timestamp for JWT + $expTimestamp = null; 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(60, $expiryDate->getTimestamp() - $now->getTimestamp()); + $secondsUntilExpiry = $expiryDate->getTimestamp() - $now->getTimestamp(); + + // If token is expired, set expiration to 1 minute from now + // We check for actual expiry later on route hooks for validation + if ($secondsUntilExpiry <= 0) { + $expTimestamp = $now->getTimestamp() + 60; + } else { + $expTimestamp = $expiryDate->getTimestamp(); + } } - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $maxAge, 10); - $secret = $jwt->encode([ + // Use maxAge as fallback, but rely on exp in payload for actual expiration + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', PHP_INT_MAX, 10); + + $payload = [ 'tokenId' => $document->getId(), 'resourceId' => $document->getAttribute('resourceId'), 'resourceType' => $document->getAttribute('resourceType'), 'resourceInternalId' => $document->getAttribute('resourceInternalId'), - ]); + ]; + + // Set explicit expiration in JWT payload if we have an expiry date + if ($expTimestamp !== null) { + $payload['exp'] = $expTimestamp; + } + + $secret = $jwt->encode($payload); $document->setAttribute('secret', $secret); diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php index 4a7aab474a..d8bcd946ef 100644 --- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php +++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php @@ -107,6 +107,44 @@ class TokensConsoleClientTest extends Scope return $data; } + /** + * @depends testCreateToken + */ + public function testExpiredTokenJWT(array $data): array + { + $fileId = $data['fileId']; + $bucketId = $data['bucketId']; + + // Create a token with an expiry date in the past (expired) + $pastExpiry = DateTime::addSeconds(new \DateTime(), -3600); // 1 hour ago + $expiredToken = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'] + ], $this->getHeaders()), [ + 'expire' => $pastExpiry, + ]); + + $this->assertEquals(201, $expiredToken['headers']['status-code']); + $this->assertEquals('files', $expiredToken['body']['resourceType']); + + // Verify that the JWT is generated without causing a 500 error + $this->assertNotEmpty($expiredToken['body']['secret']); + + // Parse the JWT to verify expiration is set correctly for expired tokens + $jwtParts = explode('.', $expiredToken['body']['secret']); + $this->assertCount(3, $jwtParts, 'JWT should have 3 parts'); + + $payload = json_decode(base64_decode($jwtParts[1]), true); + $this->assertArrayHasKey('exp', $payload, 'JWT payload should contain exp field'); + + // For expired tokens, exp should be set to a short time in the future (around 1 minute) + $now = time(); + $this->assertGreaterThan($now, $payload['exp'], 'JWT exp should be in the future even for expired tokens'); + $this->assertLessThanOrEqual($now + 120, $payload['exp'], 'JWT exp should not be more than 2 minutes in the future for expired tokens'); + + return $data; + } + /** * @depends testCreateToken */ From 3a9353a78b2fc05afbc8826c75b8f6822ee04d25 Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 15 Aug 2025 17:35:49 -0700 Subject: [PATCH 2/7] linter --- src/Appwrite/Utopia/Response/Model/ResourceToken.php | 10 +++++----- tests/e2e/Services/Tokens/TokensConsoleClientTest.php | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/ResourceToken.php b/src/Appwrite/Utopia/Response/Model/ResourceToken.php index 73c3efee72..1b9dc6281a 100644 --- a/src/Appwrite/Utopia/Response/Model/ResourceToken.php +++ b/src/Appwrite/Utopia/Response/Model/ResourceToken.php @@ -63,13 +63,13 @@ class ResourceToken extends Model { $expire = $document->getAttribute('expire'); $now = new \DateTime(); - + // Calculate expiration timestamp for JWT $expTimestamp = null; if ($expire !== null) { $expiryDate = new \DateTime($expire); $secondsUntilExpiry = $expiryDate->getTimestamp() - $now->getTimestamp(); - + // If token is expired, set expiration to 1 minute from now // We check for actual expiry later on route hooks for validation if ($secondsUntilExpiry <= 0) { @@ -81,19 +81,19 @@ class ResourceToken extends Model // Use maxAge as fallback, but rely on exp in payload for actual expiration $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', PHP_INT_MAX, 10); - + $payload = [ 'tokenId' => $document->getId(), 'resourceId' => $document->getAttribute('resourceId'), 'resourceType' => $document->getAttribute('resourceType'), 'resourceInternalId' => $document->getAttribute('resourceInternalId'), ]; - + // Set explicit expiration in JWT payload if we have an expiry date if ($expTimestamp !== null) { $payload['exp'] = $expTimestamp; } - + $secret = $jwt->encode($payload); $document->setAttribute('secret', $secret); diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php index d8bcd946ef..cf3b537575 100644 --- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php +++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php @@ -126,17 +126,17 @@ class TokensConsoleClientTest extends Scope $this->assertEquals(201, $expiredToken['headers']['status-code']); $this->assertEquals('files', $expiredToken['body']['resourceType']); - + // Verify that the JWT is generated without causing a 500 error $this->assertNotEmpty($expiredToken['body']['secret']); - + // Parse the JWT to verify expiration is set correctly for expired tokens $jwtParts = explode('.', $expiredToken['body']['secret']); $this->assertCount(3, $jwtParts, 'JWT should have 3 parts'); - + $payload = json_decode(base64_decode($jwtParts[1]), true); $this->assertArrayHasKey('exp', $payload, 'JWT payload should contain exp field'); - + // For expired tokens, exp should be set to a short time in the future (around 1 minute) $now = time(); $this->assertGreaterThan($now, $payload['exp'], 'JWT exp should be in the future even for expired tokens'); From f5f6ec71de76afa5342d7d911ec155678c0ebaf6 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 18 Aug 2025 09:39:19 -0700 Subject: [PATCH 3/7] Address stnguyen90 feedback: simplify JWT logic and fix test expectations - Remove excessive logic and 1-minute expiration hack as requested - Simplify token expiry handling to directly use existing value - Remove problematic test that creates tokens with past expiry dates - Update existing test to properly verify JWT expiration behavior - Use proper base64url decoding for JWT payload testing --- .../Utopia/Response/Model/ResourceToken.php | 25 ++----- .../Tokens/TokensConsoleClientTest.php | 66 ++++++++----------- 2 files changed, 34 insertions(+), 57 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/ResourceToken.php b/src/Appwrite/Utopia/Response/Model/ResourceToken.php index 1b9dc6281a..17d97ab0ca 100644 --- a/src/Appwrite/Utopia/Response/Model/ResourceToken.php +++ b/src/Appwrite/Utopia/Response/Model/ResourceToken.php @@ -62,25 +62,9 @@ class ResourceToken extends Model public function filter(Document $document): Document { $expire = $document->getAttribute('expire'); - $now = new \DateTime(); - // Calculate expiration timestamp for JWT - $expTimestamp = null; - if ($expire !== null) { - $expiryDate = new \DateTime($expire); - $secondsUntilExpiry = $expiryDate->getTimestamp() - $now->getTimestamp(); - - // If token is expired, set expiration to 1 minute from now - // We check for actual expiry later on route hooks for validation - if ($secondsUntilExpiry <= 0) { - $expTimestamp = $now->getTimestamp() + 60; - } else { - $expTimestamp = $expiryDate->getTimestamp(); - } - } - - // Use maxAge as fallback, but rely on exp in payload for actual expiration - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', PHP_INT_MAX, 10); + // Disable library auto-exp; rely solely on explicit exp in payload when set + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 0, 10); $payload = [ 'tokenId' => $document->getId(), @@ -90,8 +74,9 @@ class ResourceToken extends Model ]; // Set explicit expiration in JWT payload if we have an expiry date - if ($expTimestamp !== null) { - $payload['exp'] = $expTimestamp; + if ($expire !== null) { + $expiryDate = new \DateTime($expire); + $payload['exp'] = $expiryDate->getTimestamp(); } $secret = $jwt->encode($payload); diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php index cf3b537575..94529f7280 100644 --- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php +++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php @@ -94,6 +94,25 @@ class TokensConsoleClientTest extends Scope $dateValidator = new DatetimeValidator(); $this->assertTrue($dateValidator->isValid($token['body']['expire'])); + // Verify JWT contains correct expiration + $this->assertNotEmpty($token['body']['secret']); + $jwtParts = explode('.', $token['body']['secret']); + $this->assertCount(3, $jwtParts, 'JWT should have 3 parts'); + + // Decode JWT payload (using base64url decoding) + $payloadB64 = $jwtParts[1]; + // Convert base64url to base64 + $payloadB64 = str_replace(['-', '_'], ['+', '/'], $payloadB64); + // Add padding if needed + $payloadB64 .= str_repeat('=', (4 - strlen($payloadB64) % 4) % 4); + $payload = json_decode(base64_decode($payloadB64), true); + + $this->assertIsArray($payload, 'JWT payload should decode to an array'); + $this->assertArrayHasKey('exp', $payload, 'JWT payload should contain exp field'); + + $expectedExp = (new \DateTime($expiry))->getTimestamp(); + $this->assertEquals($expectedExp, $payload['exp'], 'JWT exp should match token expiry'); + // Infinite expiry $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, array_merge([ 'content-type' => 'application/json', @@ -104,47 +123,20 @@ class TokensConsoleClientTest extends Scope $this->assertEmpty($token['body']['expire']); - return $data; - } - - /** - * @depends testCreateToken - */ - public function testExpiredTokenJWT(array $data): array - { - $fileId = $data['fileId']; - $bucketId = $data['bucketId']; - - // Create a token with an expiry date in the past (expired) - $pastExpiry = DateTime::addSeconds(new \DateTime(), -3600); // 1 hour ago - $expiredToken = $this->client->call(Client::METHOD_POST, '/tokens/buckets/' . $bucketId . '/files/' . $fileId, array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'] - ], $this->getHeaders()), [ - 'expire' => $pastExpiry, - ]); - - $this->assertEquals(201, $expiredToken['headers']['status-code']); - $this->assertEquals('files', $expiredToken['body']['resourceType']); - - // Verify that the JWT is generated without causing a 500 error - $this->assertNotEmpty($expiredToken['body']['secret']); - - // Parse the JWT to verify expiration is set correctly for expired tokens - $jwtParts = explode('.', $expiredToken['body']['secret']); - $this->assertCount(3, $jwtParts, 'JWT should have 3 parts'); - - $payload = json_decode(base64_decode($jwtParts[1]), true); - $this->assertArrayHasKey('exp', $payload, 'JWT payload should contain exp field'); - - // For expired tokens, exp should be set to a short time in the future (around 1 minute) - $now = time(); - $this->assertGreaterThan($now, $payload['exp'], 'JWT exp should be in the future even for expired tokens'); - $this->assertLessThanOrEqual($now + 120, $payload['exp'], 'JWT exp should not be more than 2 minutes in the future for expired tokens'); + // Verify JWT does not contain exp for infinite expiry + $jwtParts = explode('.', $token['body']['secret']); + $payloadB64 = $jwtParts[1]; + $payloadB64 = str_replace(['-', '_'], ['+', '/'], $payloadB64); + $payloadB64 .= str_repeat('=', (4 - strlen($payloadB64) % 4) % 4); + $payload = json_decode(base64_decode($payloadB64), true); + + $this->assertArrayNotHasKey('exp', $payload, 'JWT payload should not contain exp field for infinite expiry'); return $data; } + + /** * @depends testCreateToken */ From 3ed56998762dc042e8a78ea511132d94f66c3d20 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 18 Aug 2025 10:57:15 -0700 Subject: [PATCH 4/7] Better jwt handling and tests --- src/Appwrite/Utopia/Response/Model/ResourceToken.php | 7 +++++-- tests/e2e/Services/Tokens/TokensConsoleClientTest.php | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Appwrite/Utopia/Response/Model/ResourceToken.php b/src/Appwrite/Utopia/Response/Model/ResourceToken.php index 17d97ab0ca..87ab66ab5d 100644 --- a/src/Appwrite/Utopia/Response/Model/ResourceToken.php +++ b/src/Appwrite/Utopia/Response/Model/ResourceToken.php @@ -63,8 +63,8 @@ class ResourceToken extends Model { $expire = $document->getAttribute('expire'); - // Disable library auto-exp; rely solely on explicit exp in payload when set - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 0, 10); + // Use a large but reasonable maxAge to avoid auto-exp when we set explicit exp + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400 * 365 * 10, 10); // 10 years $payload = [ 'tokenId' => $document->getId(), @@ -77,6 +77,9 @@ class ResourceToken extends Model if ($expire !== null) { $expiryDate = new \DateTime($expire); $payload['exp'] = $expiryDate->getTimestamp(); + } else { + // For infinite expiry, set 'iat' to prevent JWT library from auto-adding 'exp' + $payload['iat'] = time(); } $secret = $jwt->encode($payload); diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php index 94529f7280..a34555d9cc 100644 --- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php +++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php @@ -106,10 +106,10 @@ class TokensConsoleClientTest extends Scope // Add padding if needed $payloadB64 .= str_repeat('=', (4 - strlen($payloadB64) % 4) % 4); $payload = json_decode(base64_decode($payloadB64), true); - + $this->assertIsArray($payload, 'JWT payload should decode to an array'); $this->assertArrayHasKey('exp', $payload, 'JWT payload should contain exp field'); - + $expectedExp = (new \DateTime($expiry))->getTimestamp(); $this->assertEquals($expectedExp, $payload['exp'], 'JWT exp should match token expiry'); @@ -129,7 +129,7 @@ class TokensConsoleClientTest extends Scope $payloadB64 = str_replace(['-', '_'], ['+', '/'], $payloadB64); $payloadB64 .= str_repeat('=', (4 - strlen($payloadB64) % 4) % 4); $payload = json_decode(base64_decode($payloadB64), true); - + $this->assertArrayNotHasKey('exp', $payload, 'JWT payload should not contain exp field for infinite expiry'); return $data; From 2beb6ba0cf70618fb6a0109ea26e4975c8fab092 Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 20 Aug 2025 12:51:25 -0700 Subject: [PATCH 5/7] Update tests/e2e/Services/Tokens/TokensConsoleClientTest.php Co-authored-by: Steven Nguyen <1477010+stnguyen90@users.noreply.github.com> --- tests/e2e/Services/Tokens/TokensConsoleClientTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php index a34555d9cc..c9ad3740de 100644 --- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php +++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php @@ -135,8 +135,6 @@ class TokensConsoleClientTest extends Scope return $data; } - - /** * @depends testCreateToken */ From e6e7a728a0d1f38d9393c4691f7f45c2a8650740 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 21 Aug 2025 16:10:06 +0000 Subject: [PATCH 6/7] Enhance token JWT validation in TokensConsoleClientTest with comprehensive checks Co-authored-by: evanleair --- .../Tokens/TokensConsoleClientTest.php | 101 +++++++++++++----- 1 file changed, 77 insertions(+), 24 deletions(-) diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php index c9ad3740de..f0f9228e6d 100644 --- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php +++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php @@ -2,6 +2,8 @@ namespace Tests\E2E\Services\Tokens; +use Ahc\Jwt\JWT; +use Ahc\Jwt\JWTException; use CURLFile; use Tests\E2E\Client; use Tests\E2E\Scopes\ProjectCustom; @@ -12,6 +14,7 @@ use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Validator\Datetime as DatetimeValidator; +use Utopia\System\System; class TokensConsoleClientTest extends Scope { @@ -67,6 +70,28 @@ class TokensConsoleClientTest extends Scope $this->assertEquals(201, $token['headers']['status-code']); $this->assertEquals('files', $token['body']['resourceType']); + $this->assertNotEmpty($token['body']['$id']); + $this->assertNotEmpty($token['body']['secret']); + + // Verify the generated token JWT contains correct resource information + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400 * 365 * 10, 10); // 10 years maxAge + try { + $payload = $jwt->decode($token['body']['secret']); + $this->assertIsArray($payload, 'JWT payload should decode to an array'); + $this->assertArrayHasKey('tokenId', $payload, 'JWT payload should contain tokenId'); + $this->assertArrayHasKey('resourceId', $payload, 'JWT payload should contain resourceId'); + $this->assertArrayHasKey('resourceType', $payload, 'JWT payload should contain resourceType'); + $this->assertArrayHasKey('resourceInternalId', $payload, 'JWT payload should contain resourceInternalId'); + + $this->assertEquals($token['body']['$id'], $payload['tokenId'], 'JWT tokenId should match token ID'); + $this->assertEquals($fileId, $payload['resourceId'], 'JWT resourceId should match file ID'); + $this->assertEquals('files', $payload['resourceType'], 'JWT resourceType should be files'); + + // For newly created tokens without expiry, should not have exp field + $this->assertArrayNotHasKey('exp', $payload, 'JWT payload should not contain exp field for tokens without expiry'); + } catch (JWTException $e) { + $this->fail('Failed to decode JWT: ' . $e->getMessage()); + } return [ 'fileId' => $fileId, @@ -94,24 +119,20 @@ class TokensConsoleClientTest extends Scope $dateValidator = new DatetimeValidator(); $this->assertTrue($dateValidator->isValid($token['body']['expire'])); - // Verify JWT contains correct expiration + // Verify JWT contains correct expiration using native JWT decode $this->assertNotEmpty($token['body']['secret']); - $jwtParts = explode('.', $token['body']['secret']); - $this->assertCount(3, $jwtParts, 'JWT should have 3 parts'); + + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400 * 365 * 10, 10); // 10 years maxAge + try { + $payload = $jwt->decode($token['body']['secret']); + $this->assertIsArray($payload, 'JWT payload should decode to an array'); + $this->assertArrayHasKey('exp', $payload, 'JWT payload should contain exp field'); - // Decode JWT payload (using base64url decoding) - $payloadB64 = $jwtParts[1]; - // Convert base64url to base64 - $payloadB64 = str_replace(['-', '_'], ['+', '/'], $payloadB64); - // Add padding if needed - $payloadB64 .= str_repeat('=', (4 - strlen($payloadB64) % 4) % 4); - $payload = json_decode(base64_decode($payloadB64), true); - - $this->assertIsArray($payload, 'JWT payload should decode to an array'); - $this->assertArrayHasKey('exp', $payload, 'JWT payload should contain exp field'); - - $expectedExp = (new \DateTime($expiry))->getTimestamp(); - $this->assertEquals($expectedExp, $payload['exp'], 'JWT exp should match token expiry'); + $expectedExp = (new \DateTime($expiry))->getTimestamp(); + $this->assertEquals($expectedExp, $payload['exp'], 'JWT exp should match token expiry'); + } catch (JWTException $e) { + $this->fail('Failed to decode JWT: ' . $e->getMessage()); + } // Infinite expiry $token = $this->client->call(Client::METHOD_PATCH, '/tokens/' . $tokenId, array_merge([ @@ -123,14 +144,14 @@ class TokensConsoleClientTest extends Scope $this->assertEmpty($token['body']['expire']); - // Verify JWT does not contain exp for infinite expiry - $jwtParts = explode('.', $token['body']['secret']); - $payloadB64 = $jwtParts[1]; - $payloadB64 = str_replace(['-', '_'], ['+', '/'], $payloadB64); - $payloadB64 .= str_repeat('=', (4 - strlen($payloadB64) % 4) % 4); - $payload = json_decode(base64_decode($payloadB64), true); - - $this->assertArrayNotHasKey('exp', $payload, 'JWT payload should not contain exp field for infinite expiry'); + // Verify JWT does not contain exp for infinite expiry using native JWT decode + try { + $payload = $jwt->decode($token['body']['secret']); + $this->assertIsArray($payload, 'JWT payload should decode to an array'); + $this->assertArrayNotHasKey('exp', $payload, 'JWT payload should not contain exp field for infinite expiry'); + } catch (JWTException $e) { + $this->fail('Failed to decode JWT: ' . $e->getMessage()); + } return $data; } @@ -151,6 +172,38 @@ class TokensConsoleClientTest extends Scope $this->assertIsArray($res['body']); $this->assertEquals(200, $res['headers']['status-code']); + $this->assertArrayHasKey('tokens', $res['body']); + $this->assertIsArray($res['body']['tokens']); + $this->assertGreaterThan(0, count($res['body']['tokens']), 'Should have at least one token'); + + // Verify each token in the list + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400 * 365 * 10, 10); // 10 years maxAge + foreach ($res['body']['tokens'] as $token) { + $this->assertArrayHasKey('$id', $token, 'Token should have an ID'); + $this->assertArrayHasKey('secret', $token, 'Token should have a secret'); + $this->assertArrayHasKey('resourceType', $token, 'Token should have resourceType'); + $this->assertArrayHasKey('resourceId', $token, 'Token should have resourceId'); + + $this->assertEquals('files', $token['resourceType'], 'Token resourceType should be files'); + $this->assertEquals($data['fileId'], $token['resourceId'], 'Token resourceId should match file ID'); + + // Verify the JWT token is valid and contains correct information + try { + $payload = $jwt->decode($token['secret']); + $this->assertIsArray($payload, 'JWT payload should decode to an array'); + $this->assertArrayHasKey('tokenId', $payload, 'JWT payload should contain tokenId'); + $this->assertArrayHasKey('resourceId', $payload, 'JWT payload should contain resourceId'); + $this->assertArrayHasKey('resourceType', $payload, 'JWT payload should contain resourceType'); + $this->assertArrayHasKey('resourceInternalId', $payload, 'JWT payload should contain resourceInternalId'); + + $this->assertEquals($token['$id'], $payload['tokenId'], 'JWT tokenId should match token ID'); + $this->assertEquals($data['fileId'], $payload['resourceId'], 'JWT resourceId should match file ID'); + $this->assertEquals('files', $payload['resourceType'], 'JWT resourceType should be files'); + } catch (JWTException $e) { + $this->fail('Failed to decode JWT for token ' . $token['$id'] . ': ' . $e->getMessage()); + } + } + return $data; } From 0371e488e918f6ed2ed87ec9f9e0642ffc577809 Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 21 Aug 2025 11:02:53 -0700 Subject: [PATCH 7/7] test fix/lint --- .../Tokens/TokensConsoleClientTest.php | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php index f0f9228e6d..c0f94a55bf 100644 --- a/tests/e2e/Services/Tokens/TokensConsoleClientTest.php +++ b/tests/e2e/Services/Tokens/TokensConsoleClientTest.php @@ -72,7 +72,7 @@ class TokensConsoleClientTest extends Scope $this->assertEquals('files', $token['body']['resourceType']); $this->assertNotEmpty($token['body']['$id']); $this->assertNotEmpty($token['body']['secret']); - + // Verify the generated token JWT contains correct resource information $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400 * 365 * 10, 10); // 10 years maxAge try { @@ -82,11 +82,11 @@ class TokensConsoleClientTest extends Scope $this->assertArrayHasKey('resourceId', $payload, 'JWT payload should contain resourceId'); $this->assertArrayHasKey('resourceType', $payload, 'JWT payload should contain resourceType'); $this->assertArrayHasKey('resourceInternalId', $payload, 'JWT payload should contain resourceInternalId'); - + $this->assertEquals($token['body']['$id'], $payload['tokenId'], 'JWT tokenId should match token ID'); - $this->assertEquals($fileId, $payload['resourceId'], 'JWT resourceId should match file ID'); + $this->assertEquals($bucketId . ':' . $fileId, $payload['resourceId'], 'JWT resourceId should match bucketId:fileId format'); $this->assertEquals('files', $payload['resourceType'], 'JWT resourceType should be files'); - + // For newly created tokens without expiry, should not have exp field $this->assertArrayNotHasKey('exp', $payload, 'JWT payload should not contain exp field for tokens without expiry'); } catch (JWTException $e) { @@ -121,7 +121,7 @@ class TokensConsoleClientTest extends Scope // Verify JWT contains correct expiration using native JWT decode $this->assertNotEmpty($token['body']['secret']); - + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400 * 365 * 10, 10); // 10 years maxAge try { $payload = $jwt->decode($token['body']['secret']); @@ -175,7 +175,7 @@ class TokensConsoleClientTest extends Scope $this->assertArrayHasKey('tokens', $res['body']); $this->assertIsArray($res['body']['tokens']); $this->assertGreaterThan(0, count($res['body']['tokens']), 'Should have at least one token'); - + // Verify each token in the list $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 86400 * 365 * 10, 10); // 10 years maxAge foreach ($res['body']['tokens'] as $token) { @@ -183,10 +183,10 @@ class TokensConsoleClientTest extends Scope $this->assertArrayHasKey('secret', $token, 'Token should have a secret'); $this->assertArrayHasKey('resourceType', $token, 'Token should have resourceType'); $this->assertArrayHasKey('resourceId', $token, 'Token should have resourceId'); - + $this->assertEquals('files', $token['resourceType'], 'Token resourceType should be files'); - $this->assertEquals($data['fileId'], $token['resourceId'], 'Token resourceId should match file ID'); - + $this->assertEquals($data['bucketId'] . ':' . $data['fileId'], $token['resourceId'], 'Token resourceId should match bucketId:fileId format'); + // Verify the JWT token is valid and contains correct information try { $payload = $jwt->decode($token['secret']); @@ -195,15 +195,15 @@ class TokensConsoleClientTest extends Scope $this->assertArrayHasKey('resourceId', $payload, 'JWT payload should contain resourceId'); $this->assertArrayHasKey('resourceType', $payload, 'JWT payload should contain resourceType'); $this->assertArrayHasKey('resourceInternalId', $payload, 'JWT payload should contain resourceInternalId'); - + $this->assertEquals($token['$id'], $payload['tokenId'], 'JWT tokenId should match token ID'); - $this->assertEquals($data['fileId'], $payload['resourceId'], 'JWT resourceId should match file ID'); + $this->assertEquals($data['bucketId'] . ':' . $data['fileId'], $payload['resourceId'], 'JWT resourceId should match bucketId:fileId format'); $this->assertEquals('files', $payload['resourceType'], 'JWT resourceType should be files'); } catch (JWTException $e) { $this->fail('Failed to decode JWT for token ' . $token['$id'] . ': ' . $e->getMessage()); } } - + return $data; }