From 613677e9f77af58349d6e48df0d1b8c350d6905d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Mon, 27 May 2024 20:04:50 +0000 Subject: [PATCH 1/6] Implement users.createJWT --- app/controllers/api/account.php | 3 +- app/controllers/api/users.php | 55 ++++++++++++++++++++++++ app/init.php | 4 +- docs/references/users/create-user-jwt.md | 1 + 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 docs/references/users/create-user-jwt.md diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 6190cec905..93ec8e2aff 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2336,7 +2336,8 @@ App::post('/v1/account/tokens/phone') ; }); -App::post('/v1/account/jwt') +App::post('/v1/account/jwts') + ->alias('/v1/account/jwt') ->desc('Create JWT') ->groups(['api', 'account', 'auth']) ->label('scope', 'account') diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index abfcb34a5a..ca34a696f0 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -1,5 +1,6 @@ noContent(); }); +App::post('/v1/users/:userId/jwts') + ->desc('Create user JWT') + ->groups(['api', 'users']) + ->label('scope', 'users.write') + ->label('sdk.auth', [APP_AUTH_TYPE_KEY]) + ->label('sdk.namespace', 'users') + ->label('sdk.method', 'createJWT') + ->label('sdk.description', '/docs/references/users/create-user-jwt.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_JWT) + ->param('userId', '', new UID(), 'User ID.') + ->param('sessionId', 'current', new UID(), 'Session ID. Use the string \'current\' to use the most recent session. Defaults to the most recent session.') + ->param('duration', 900, new Range(0, 3600), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.') + ->inject('response') + ->inject('dbForProject') + ->action(function (string $userId, int $duration, string $sessionId, Response $response, Database $dbForProject) { + + $user = $dbForProject->getDocument('users', $userId); + + if ($user->isEmpty()) { + throw new Exception(Exception::USER_NOT_FOUND); + } + + $sessions = $user->getAttribute('sessions', []); + $session = new Document(); + + if($sessionId === 'current') { + // Get most recent + $session = \count($sessions) > 0 ? $sessions[\count($sessions) - 1] : new Document(); + } else { + // Find by ID + foreach ($sessions as $loopSession) { /** @var Utopia\Database\Document $loopSession */ + if ($loopSession->getId() == $sessionId) { + $session = $loopSession; + } + } + } + + if ($session->isEmpty()) { + throw new Exception(Exception::USER_SESSION_NOT_FOUND); + } + + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 10); // Instantiate with key, algo, maxAge and leeway. + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic(new Document(['jwt' => $jwt->encode([ + 'userId' => $user->getId(), + 'sessionId' => $session->getId(), + ])]), Response::MODEL_JWT); + }); + App::get('/v1/users/usage') ->desc('Get users usage stats') ->groups(['api', 'users']) diff --git a/app/init.php b/app/init.php index 623c4bd14f..3f85b1786a 100644 --- a/app/init.php +++ b/app/init.php @@ -440,10 +440,12 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - return Authorization::skip(fn () => $database->find('sessions', [ + $s = Authorization::skip(fn () => $database->find('sessions', [ Query::equal('userInternalId', [$document->getInternalId()]), Query::limit(APP_LIMIT_SUBQUERY), ])); + \var_dump($s); + return $s; } ); diff --git a/docs/references/users/create-user-jwt.md b/docs/references/users/create-user-jwt.md new file mode 100644 index 0000000000..5e8c26c8f7 --- /dev/null +++ b/docs/references/users/create-user-jwt.md @@ -0,0 +1 @@ +Use this endpoint to create a JSON Web Token for user by its unique ID. You can use the resulting JWT to authenticate on behalf of the user. The JWT secret will become invalid if the session it uses gets deleted. \ No newline at end of file From b1ff989c3fd67e40363126eb03e8e243cc2e590d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 May 2024 09:25:54 +0000 Subject: [PATCH 2/6] Implement tests, fix JWT maxAge --- app/controllers/api/account.php | 7 +- app/controllers/api/functions.php | 7 +- app/controllers/api/messaging.php | 8 +- app/controllers/api/storage.php | 6 +- app/controllers/api/users.php | 11 +- app/controllers/shared/api.php | 6 +- app/init.php | 22 +++- src/Appwrite/Platform/Workers/Functions.php | 3 +- tests/e2e/Services/Users/UsersBase.php | 131 ++++++++++++++++++++ 9 files changed, 178 insertions(+), 23 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 93ec8e2aff..35f8979b25 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2370,15 +2370,12 @@ App::post('/v1/account/jwts') throw new Exception(Exception::USER_SESSION_NOT_FOUND); } - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); // Instantiate with key, algo, maxAge and leeway. $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic(new Document(['jwt' => $jwt->encode([ - // 'uid' => 1, - // 'aud' => 'http://site.com', - // 'scopes' => ['user'], - // 'iss' => 'http://api.mysite.com', + 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT900S'))->format('U')), 'userId' => $user->getId(), 'sessionId' => $current->getId(), ])]), Response::MODEL_JWT); diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 670ca2f99e..8787cba8dc 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -1585,8 +1585,10 @@ App::post('/v1/functions/:functionId/executions') } if (!$current->isEmpty()) { - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. + $jwtExpiry = $function->getAttribute('timeout', 900); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); // Instantiate with key, algo, maxAge and leeway. $jwt = $jwtObj->encode([ + 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT' . $jwtExpiry . 'S'))->format('U')), 'userId' => $user->getId(), 'sessionId' => $current->getId(), ]); @@ -1594,8 +1596,9 @@ App::post('/v1/functions/:functionId/executions') } $jwtExpiry = $function->getAttribute('timeout', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 10); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); $apiKey = $jwtObj->encode([ + 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT' . $jwtExpiry . 'S'))->format('U')), 'projectId' => $project->getId(), 'scopes' => $function->getAttribute('scopes', []) ]); diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index ceb2d2aca5..fc8f9292be 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -2939,11 +2939,11 @@ App::post('/v1/messaging/messages/push') $expiry = (new \DateTime())->add(new \DateInterval('P15D'))->format('U'); } - $encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1')); + $encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); $jwt = $encoder->encode([ 'iat' => \time(), - 'exp' => $expiry, + 'exp' => \intval($expiry), 'bucketId' => $bucket->getId(), 'fileId' => $file->getId(), 'projectId' => $project->getId(), @@ -3801,11 +3801,11 @@ App::patch('/v1/messaging/messages/push/:messageId') $expiry = (new \DateTime())->add(new \DateInterval('P15D'))->format('U'); } - $encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1')); + $encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); $jwt = $encoder->encode([ 'iat' => \time(), - 'exp' => $expiry, + 'exp' => \intval($expiry), 'bucketId' => $bucket->getId(), 'fileId' => $file->getId(), 'projectId' => $project->getId(), diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index f7334e3ebb..cba55b8c87 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -1328,7 +1328,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') ->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1')); + $decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); try { $decoded = $decoder->decode($jwt); @@ -1336,6 +1336,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') throw new Exception(Exception::USER_UNAUTHORIZED); } + if($decoded['exp'] < \time()) { + throw new Exception(Exception::USER_UNAUTHORIZED); + } + if ( $decoded['projectId'] !== $project->getId() || $decoded['bucketId'] !== $bucketId || diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index ca34a696f0..02a9d45962 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -2105,11 +2105,11 @@ App::post('/v1/users/:userId/jwts') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_JWT) ->param('userId', '', new UID(), 'User ID.') - ->param('sessionId', 'current', new UID(), 'Session ID. Use the string \'current\' to use the most recent session. Defaults to the most recent session.') - ->param('duration', 900, new Range(0, 3600), 'Time in seconds before JWT expires. Default duration is 900 seconds, and maximum is 3600 seconds.') + ->param('sessionId', 'current', new UID(), 'Session ID. Use the string \'current\' to use the most recent session. Defaults to the most recent session.', true) + ->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('dbForProject') - ->action(function (string $userId, int $duration, string $sessionId, Response $response, Database $dbForProject) { + ->action(function (string $userId, string $sessionId, int $duration, Response $response, Database $dbForProject) { $user = $dbForProject->getDocument('users', $userId); @@ -2136,13 +2136,14 @@ App::post('/v1/users/:userId/jwts') throw new Exception(Exception::USER_SESSION_NOT_FOUND); } - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 10); // Instantiate with key, algo, maxAge and leeway. + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); // Instantiate with key, algo, maxAge and leeway. $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic(new Document(['jwt' => $jwt->encode([ + 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT' . $duration . 'S'))->format('U')), 'userId' => $user->getId(), - 'sessionId' => $session->getId(), + 'sessionId' => $session->getId() ])]), Response::MODEL_JWT); }); diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index fc6792a913..3a1bfce7e2 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -216,7 +216,7 @@ App::init() if($keyType === API_KEY_DYNAMIC) { // Dynamic key - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); try { $payload = $jwtObj->decode($authKey); @@ -224,6 +224,10 @@ App::init() throw new Exception(Exception::API_KEY_EXPIRED); } + if($payload['exp'] < \time()) { + throw new Exception(Exception::API_KEY_EXPIRED); + } + $projectId = $payload['projectId'] ?? ''; $tokenScopes = $payload['scopes'] ?? []; diff --git a/app/init.php b/app/init.php index 3f85b1786a..7fda855d72 100644 --- a/app/init.php +++ b/app/init.php @@ -440,12 +440,10 @@ Database::addFilter( return; }, function (mixed $value, Document $document, Database $database) { - $s = Authorization::skip(fn () => $database->find('sessions', [ + return Authorization::skip(fn () => $database->find('sessions', [ Query::equal('userInternalId', [$document->getInternalId()]), Query::limit(APP_LIMIT_SUBQUERY), ])); - \var_dump($s); - return $s; } ); @@ -1202,7 +1200,7 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons $authJWT = $request->getHeader('x-appwrite-jwt', ''); if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); // Instantiate with key, algo, maxAge and leeway. try { $payload = $jwt->decode($authJWT); @@ -1220,6 +1218,22 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token $user = new Document([]); } + + $exp = $payload['exp'] ?? ''; + + // Fallback to 15m, just in case + if(empty($exp)) { + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. + try { + $payload = $jwt->decode($authJWT); + } catch (JWTException $error) { + $user = new Document([]); + } + } else { + if($exp < \time()) { + $user = new Document([]); + } + } } $dbForProject->setMetadata('user', $user->getId()); diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index fc9a1242ac..7b0d45e3c5 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -284,8 +284,9 @@ class Functions extends Action $runtime = $runtimes[$function->getAttribute('runtime')]; $jwtExpiry = $function->getAttribute('timeout', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 10); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); $apiKey = $jwtObj->encode([ + 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT' . $jwtExpiry . 'S'))->format('U')), 'projectId' => $project->getId(), 'scopes' => $function->getAttribute('scopes', []) ]); diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php index 1737e0483d..6df3a7250c 100644 --- a/tests/e2e/Services/Users/UsersBase.php +++ b/tests/e2e/Services/Users/UsersBase.php @@ -1553,6 +1553,137 @@ trait UsersBase return $data; } + public function testUsetJWT() + { + // Create user + $userId = ID::unique(); + $user = $this->client->call(Client::METHOD_POST, '/users', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'userId' => $userId, + 'email' => 'jwtuser@appwrite.io', + 'password' => 'password', + ], false); + $this->assertEquals($user['headers']['status-code'], 201); + + // Create two sessions + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => 'jwtuser@appwrite.io', + 'password' => 'password', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals($userId, $response['body']['userId']); + $this->assertNotEmpty($response['body']['$id']); + $session1Id = $response['body']['$id']; + + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => 'jwtuser@appwrite.io', + 'password' => 'password', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertEquals($userId, $response['body']['userId']); + $this->assertNotEmpty($response['body']['$id']); + $session2Id = $response['body']['$id']; + + // Create JWT 1 for older session by ID + $response = $this->client->call(Client::METHOD_POST, '/users/' . $userId . '/jwts', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'sessionId' => $session1Id + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['jwt']); + $jwt1 = $response['body']['jwt']; + + // Ensure JWT 1 works + $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-jwt' => $jwt1, + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($userId, $response['body']['$id']); + + // Create JWT 2 for latest session using default param + $response = $this->client->call(Client::METHOD_POST, '/users/' . $userId . '/jwts', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'duration' => 5 + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + $this->assertNotEmpty($response['body']['jwt']); + $jwt2 = $response['body']['jwt']; + + // Ensure JWT 2 works + $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-jwt' => $jwt2, + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($userId, $response['body']['$id']); + + // Wait, ensure JWT 2 no longer works because of short duration + + \sleep(10); + $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-jwt' => $jwt2, + ])); + + $this->assertEquals(401, $response['headers']['status-code']); + + // Delete session, ensure JWT 1 no longer works because of session missing + + $response = $this->client->call(Client::METHOD_DELETE, '/users/' . $userId . '/sessions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'sessionId' => $session1Id + ]); + + $this->assertEquals(204, $response['headers']['status-code']); + + $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-jwt' => $jwt1, + ])); + + $this->assertEquals(401, $response['headers']['status-code']); + + // Cleanup after test + + $response = $this->client->call(Client::METHOD_DELETE, '/users/' . $userId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals($response['headers']['status-code'], 204); + } + // TODO add test for session delete // TODO add test for all sessions delete } From 711e26c605ea52220dc83594de0300721c5c6a20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Tue, 28 May 2024 10:59:53 +0000 Subject: [PATCH 3/6] Relay on lib to verify JWT expiry --- app/controllers/api/account.php | 3 ++- app/controllers/api/functions.php | 6 ++++-- app/controllers/api/messaging.php | 4 ++-- app/controllers/api/storage.php | 9 ++------- app/controllers/api/users.php | 3 ++- app/controllers/shared/api.php | 6 +----- app/init.php | 18 +----------------- src/Appwrite/Platform/Workers/Functions.php | 3 ++- 8 files changed, 16 insertions(+), 36 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 35f8979b25..96b169bbbb 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2370,11 +2370,12 @@ App::post('/v1/account/jwts') throw new Exception(Exception::USER_SESSION_NOT_FOUND); } - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); // Instantiate with key, algo, maxAge and leeway. + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic(new Document(['jwt' => $jwt->encode([ + 'iat' => \time(), 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT900S'))->format('U')), 'userId' => $user->getId(), 'sessionId' => $current->getId(), diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 8787cba8dc..04375a6b71 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -1586,8 +1586,9 @@ App::post('/v1/functions/:functionId/executions') if (!$current->isEmpty()) { $jwtExpiry = $function->getAttribute('timeout', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); // Instantiate with key, algo, maxAge and leeway. + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); $jwt = $jwtObj->encode([ + 'iat' => \time(), 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT' . $jwtExpiry . 'S'))->format('U')), 'userId' => $user->getId(), 'sessionId' => $current->getId(), @@ -1596,8 +1597,9 @@ App::post('/v1/functions/:functionId/executions') } $jwtExpiry = $function->getAttribute('timeout', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); $apiKey = $jwtObj->encode([ + 'iat' => \time(), 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT' . $jwtExpiry . 'S'))->format('U')), 'projectId' => $project->getId(), 'scopes' => $function->getAttribute('scopes', []) diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index fc8f9292be..5676454abf 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -2939,7 +2939,7 @@ App::post('/v1/messaging/messages/push') $expiry = (new \DateTime())->add(new \DateInterval('P15D'))->format('U'); } - $encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); + $encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); $jwt = $encoder->encode([ 'iat' => \time(), @@ -3801,7 +3801,7 @@ App::patch('/v1/messaging/messages/push/:messageId') $expiry = (new \DateTime())->add(new \DateInterval('P15D'))->format('U'); } - $encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); + $encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); $jwt = $encoder->encode([ 'iat' => \time(), diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index cba55b8c87..8ee7386d98 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -1328,7 +1328,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') ->action(function (string $bucketId, string $fileId, string $jwt, Response $response, Request $request, Database $dbForProject, Document $project, string $mode, Device $deviceForFiles) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); - $decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); + $decoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); try { $decoded = $decoder->decode($jwt); @@ -1336,15 +1336,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/push') throw new Exception(Exception::USER_UNAUTHORIZED); } - if($decoded['exp'] < \time()) { - throw new Exception(Exception::USER_UNAUTHORIZED); - } - if ( $decoded['projectId'] !== $project->getId() || $decoded['bucketId'] !== $bucketId || - $decoded['fileId'] !== $fileId || - $decoded['exp'] < \time() + $decoded['fileId'] !== $fileId ) { throw new Exception(Exception::USER_UNAUTHORIZED); } diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 02a9d45962..5d7865b45b 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -2136,11 +2136,12 @@ App::post('/v1/users/:userId/jwts') throw new Exception(Exception::USER_SESSION_NOT_FOUND); } - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); // Instantiate with key, algo, maxAge and leeway. + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic(new Document(['jwt' => $jwt->encode([ + 'iat' => \time(), 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT' . $duration . 'S'))->format('U')), 'userId' => $user->getId(), 'sessionId' => $session->getId() diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 3a1bfce7e2..f3351443c9 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -216,7 +216,7 @@ App::init() if($keyType === API_KEY_DYNAMIC) { // Dynamic key - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); try { $payload = $jwtObj->decode($authKey); @@ -224,10 +224,6 @@ App::init() throw new Exception(Exception::API_KEY_EXPIRED); } - if($payload['exp'] < \time()) { - throw new Exception(Exception::API_KEY_EXPIRED); - } - $projectId = $payload['projectId'] ?? ''; $tokenScopes = $payload['scopes'] ?? []; diff --git a/app/init.php b/app/init.php index 7fda855d72..c43e60b5cf 100644 --- a/app/init.php +++ b/app/init.php @@ -1200,7 +1200,7 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons $authJWT = $request->getHeader('x-appwrite-jwt', ''); if (!empty($authJWT) && !$project->isEmpty()) { // JWT authentication - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); // Instantiate with key, algo, maxAge and leeway. + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); try { $payload = $jwt->decode($authJWT); @@ -1218,22 +1218,6 @@ App::setResource('user', function ($mode, $project, $console, $request, $respons if (empty($user->find('$id', $jwtSessionId, 'sessions'))) { // Match JWT to active token $user = new Document([]); } - - $exp = $payload['exp'] ?? ''; - - // Fallback to 15m, just in case - if(empty($exp)) { - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 10); // Instantiate with key, algo, maxAge and leeway. - try { - $payload = $jwt->decode($authJWT); - } catch (JWTException $error) { - $user = new Document([]); - } - } else { - if($exp < \time()) { - $user = new Document([]); - } - } } $dbForProject->setMetadata('user', $user->getId()); diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index 7b0d45e3c5..c281b10b37 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -284,8 +284,9 @@ class Functions extends Action $runtime = $runtimes[$function->getAttribute('runtime')]; $jwtExpiry = $function->getAttribute('timeout', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 10); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); $apiKey = $jwtObj->encode([ + 'iat' => \time(), 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT' . $jwtExpiry . 'S'))->format('U')), 'projectId' => $project->getId(), 'scopes' => $function->getAttribute('scopes', []) From 2dac8bc7ed80f5e9a258105fce75e4a7bd806487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 29 May 2024 09:31:08 +0200 Subject: [PATCH 4/6] Update app/controllers/api/users.php Co-authored-by: Steven Nguyen <1477010+stnguyen90@users.noreply.github.com> --- app/controllers/api/users.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 5d7865b45b..633f644c12 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -2128,6 +2128,7 @@ App::post('/v1/users/:userId/jwts') foreach ($sessions as $loopSession) { /** @var Utopia\Database\Document $loopSession */ if ($loopSession->getId() == $sessionId) { $session = $loopSession; + break; } } } From 11827216ffcb879a56c2bc91ee57a1335320d1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 29 May 2024 09:32:58 +0200 Subject: [PATCH 5/6] Update tests/e2e/Services/Users/UsersBase.php Co-authored-by: Steven Nguyen <1477010+stnguyen90@users.noreply.github.com> --- tests/e2e/Services/Users/UsersBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Users/UsersBase.php b/tests/e2e/Services/Users/UsersBase.php index 6df3a7250c..50f2578800 100644 --- a/tests/e2e/Services/Users/UsersBase.php +++ b/tests/e2e/Services/Users/UsersBase.php @@ -1553,7 +1553,7 @@ trait UsersBase return $data; } - public function testUsetJWT() + public function testUserJWT() { // Create user $userId = ID::unique(); From 3b8799353d825bd5efd9ff6f6d924864cc49ab3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 29 May 2024 07:51:51 +0000 Subject: [PATCH 6/6] PR review changes --- app/controllers/api/account.php | 4 +--- app/controllers/api/functions.php | 8 ++------ app/controllers/api/messaging.php | 8 ++------ app/controllers/api/users.php | 8 +++----- src/Appwrite/Platform/Workers/Functions.php | 4 +--- 5 files changed, 9 insertions(+), 23 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 96b169bbbb..6504052f9a 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -2370,13 +2370,11 @@ App::post('/v1/account/jwts') throw new Exception(Exception::USER_SESSION_NOT_FOUND); } - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 900, 0); $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic(new Document(['jwt' => $jwt->encode([ - 'iat' => \time(), - 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT900S'))->format('U')), 'userId' => $user->getId(), 'sessionId' => $current->getId(), ])]), Response::MODEL_JWT); diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 04375a6b71..52b68ab156 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -1586,10 +1586,8 @@ App::post('/v1/functions/:functionId/executions') if (!$current->isEmpty()) { $jwtExpiry = $function->getAttribute('timeout', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); $jwt = $jwtObj->encode([ - 'iat' => \time(), - 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT' . $jwtExpiry . 'S'))->format('U')), 'userId' => $user->getId(), 'sessionId' => $current->getId(), ]); @@ -1597,10 +1595,8 @@ App::post('/v1/functions/:functionId/executions') } $jwtExpiry = $function->getAttribute('timeout', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); $apiKey = $jwtObj->encode([ - 'iat' => \time(), - 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT' . $jwtExpiry . 'S'))->format('U')), 'projectId' => $project->getId(), 'scopes' => $function->getAttribute('scopes', []) ]); diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index 5676454abf..e3696cc7e0 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -2939,11 +2939,9 @@ App::post('/v1/messaging/messages/push') $expiry = (new \DateTime())->add(new \DateInterval('P15D'))->format('U'); } - $encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); + $encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', \intval($expiry), 0); $jwt = $encoder->encode([ - 'iat' => \time(), - 'exp' => \intval($expiry), 'bucketId' => $bucket->getId(), 'fileId' => $file->getId(), 'projectId' => $project->getId(), @@ -3801,11 +3799,9 @@ App::patch('/v1/messaging/messages/push/:messageId') $expiry = (new \DateTime())->add(new \DateInterval('P15D'))->format('U'); } - $encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); + $encoder = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', \intval($expiry), 0); $jwt = $encoder->encode([ - 'iat' => \time(), - 'exp' => \intval($expiry), 'bucketId' => $bucket->getId(), 'fileId' => $file->getId(), 'projectId' => $project->getId(), diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index 633f644c12..5d86beaf82 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -2105,7 +2105,7 @@ App::post('/v1/users/:userId/jwts') ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_JWT) ->param('userId', '', new UID(), 'User ID.') - ->param('sessionId', 'current', new UID(), 'Session ID. Use the string \'current\' to use the most recent session. Defaults to the most recent session.', true) + ->param('sessionId', 'recent', new UID(), 'Session ID. Use the string \'recent\' to use the most recent session. Defaults to the most recent session.', true) ->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('dbForProject') @@ -2120,7 +2120,7 @@ App::post('/v1/users/:userId/jwts') $sessions = $user->getAttribute('sessions', []); $session = new Document(); - if($sessionId === 'current') { + if($sessionId === 'recent') { // Get most recent $session = \count($sessions) > 0 ? $sessions[\count($sessions) - 1] : new Document(); } else { @@ -2137,13 +2137,11 @@ App::post('/v1/users/:userId/jwts') throw new Exception(Exception::USER_SESSION_NOT_FOUND); } - $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); + $jwt = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $duration, 0); $response ->setStatusCode(Response::STATUS_CODE_CREATED) ->dynamic(new Document(['jwt' => $jwt->encode([ - 'iat' => \time(), - 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT' . $duration . 'S'))->format('U')), 'userId' => $user->getId(), 'sessionId' => $session->getId() ])]), Response::MODEL_JWT); diff --git a/src/Appwrite/Platform/Workers/Functions.php b/src/Appwrite/Platform/Workers/Functions.php index c281b10b37..cbba9657ad 100644 --- a/src/Appwrite/Platform/Workers/Functions.php +++ b/src/Appwrite/Platform/Workers/Functions.php @@ -284,10 +284,8 @@ class Functions extends Action $runtime = $runtimes[$function->getAttribute('runtime')]; $jwtExpiry = $function->getAttribute('timeout', 900); - $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', 3600, 0); + $jwtObj = new JWT(System::getEnv('_APP_OPENSSL_KEY_V1'), 'HS256', $jwtExpiry, 0); $apiKey = $jwtObj->encode([ - 'iat' => \time(), - 'exp' => \intval((new \DateTime())->add(new \DateInterval('PT' . $jwtExpiry . 'S'))->format('U')), 'projectId' => $project->getId(), 'scopes' => $function->getAttribute('scopes', []) ]);