Implement tests, fix JWT maxAge

This commit is contained in:
Matej Bačo
2024-05-28 09:25:54 +00:00
parent 613677e9f7
commit b1ff989c3f
9 changed files with 178 additions and 23 deletions
+2 -5
View File
@@ -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);
+5 -2
View File
@@ -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', [])
]);
+4 -4
View File
@@ -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(),
+5 -1
View File
@@ -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 ||
+6 -5
View File
@@ -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);
});
+5 -1
View File
@@ -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'] ?? [];
+18 -4
View File
@@ -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());
+2 -1
View File
@@ -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', [])
]);
+131
View File
@@ -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
}