added invalidating sessions for the project users

This commit is contained in:
ArnabChatterjee20k 2025-06-14 18:07:42 +05:30
parent bc7b310c51
commit fde2f278e8
7 changed files with 108 additions and 3 deletions

View file

@ -94,13 +94,14 @@ App::post('/v1/projects')
->param('legalCity', '', new Text(256), 'Project legal City. Max length: 256 chars.', true)
->param('legalAddress', '', new Text(256), 'Project legal Address. Max length: 256 chars.', true)
->param('legalTaxId', '', new Text(256), 'Project legal Tax ID. Max length: 256 chars.', true)
->param('onPasswordChange', false, new Boolean(), 'For invalding sessions', true)
->inject('request')
->inject('response')
->inject('dbForPlatform')
->inject('cache')
->inject('pools')
->inject('hooks')
->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks) {
->action(function (string $projectId, string $name, string $teamId, string $region, string $description, string $logo, string $url, string $legalName, string $legalCountry, string $legalState, string $legalCity, string $legalAddress, string $legalTaxId, $onPasswordChange, Request $request, Response $response, Database $dbForPlatform, Cache $cache, Group $pools, Hooks $hooks) {
$team = $dbForPlatform->getDocument('teams', $teamId);
@ -127,6 +128,7 @@ App::post('/v1/projects')
'membershipsUserName' => false,
'membershipsUserEmail' => false,
'membershipsMfa' => false,
'onPasswordChange' => $onPasswordChange
];
foreach ($auth as $method) {
@ -2499,3 +2501,40 @@ App::delete('/v1/projects/:projectId/templates/email/:type/:locale')
'message' => $template['message']
]), Response::MODEL_EMAIL_TEMPLATE);
});
App::patch('/v1/projects/:projectId/auth/password-change')
->desc('Update on password change of the project')
->groups(['api', 'projects'])
->label('scope', 'projects.write')
->label('sdk', new Method(
namespace: 'projects',
group: 'auth',
name: 'updateOnPasswordChange',
description: '/docs/references/projects/update-auth-on-password-change.md',
auth: [AuthType::ADMIN],
responses: [
new SDKResponse(
code: Response::STATUS_CODE_OK,
model: Response::MODEL_PROJECT,
)
]
))
->param('projectId', '', new UID(), 'Project unique ID.')
->param('onPasswordChange', false, new Boolean(), 'For invalidating project session')
->inject('response')
->inject('dbForPlatform')
->action(function (string $projectId, bool $onPasswordChange, Response $response, Database $dbForPlatform) {
$project = $dbForPlatform->getDocument('projects', $projectId);
if ($project->isEmpty()) {
throw new Exception(Exception::PROJECT_NOT_FOUND);
}
$auths = $project->getAttribute('auths', []);
$auths['onPasswordChange'] = $onPasswordChange;
$dbForPlatform->updateDocument('projects', $project->getId(), $project
->setAttribute('auths', $auths));
$response->dynamic($project, Response::MODEL_PROJECT);
});

View file

@ -1351,6 +1351,17 @@ App::patch('/v1/users/:userId/password')
$user = $dbForProject->updateDocument('users', $user->getId(), $user);
$sessions = $user->getAttribute('sessions', []);
$onPasswordChange = $project->getAttribute('auths', [])['onPasswordChange'];
if ($onPasswordChange) {
foreach ($sessions as $session) {
/** @var Document $session */
$dbForProject->deleteDocument('sessions', $session->getId());
}
}
$dbForProject->purgeCachedDocument('users', $user->getId());
$queueForEvents->setParam('userId', $user->getId());
$response->dynamic($user, Response::MODEL_USER);

View file

@ -0,0 +1 @@
On password change. Should be an optional auth security setting for projects, and enabled by default for console project.

View file

@ -271,6 +271,12 @@ class Project extends Model
'default' => '',
'example' => self::TYPE_DATETIME_EXAMPLE,
])
->addRule('onPasswordChange', [
'type' => self::TYPE_BOOLEAN,
'description' => 'For invalidating all sessions',
'default' => false,
'example' => self::TYPE_BOOLEAN,
])
;
$services = Config::getParam('services', []);
@ -376,6 +382,7 @@ class Project extends Model
$document->setAttribute('authMembershipsUserName', $authValues['membershipsUserName'] ?? true);
$document->setAttribute('authMembershipsUserEmail', $authValues['membershipsUserEmail'] ?? true);
$document->setAttribute('authMembershipsMfa', $authValues['membershipsMfa'] ?? true);
$document->setAttribute('onPasswordChange', $authValues['onPasswordChange'] ?? false);
foreach ($auth as $index => $method) {
$key = $method['key'];

View file

@ -200,4 +200,17 @@ trait ProjectCustom
return $key['body']['secret'];
}
public function updateProjectOnPasswordChangeProperty(bool $value)
{
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . self::$project['$id'] . '/auth/password-change', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'cookie' => 'a_session_console=' . $this->getRoot()['session'],
'x-appwrite-project' => 'console',
]), [
'onPasswordChange' => $value,
]);
return $response['headers']['status-code'];
}
}

View file

@ -951,6 +951,31 @@ class ProjectsConsoleClientTest extends Scope
return ['projectId' => $projectId];
}
/** @depends testCreateProject */
public function testUpdateProjectOnPasswordChange($data): array
{
$id = $data['projectId'];
// Check defaults
$response = $this->client->call(Client::METHOD_GET, '/projects/' . $id, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertFalse($response['body']['onPasswordChange']);
$response = $this->client->call(Client::METHOD_PATCH, '/projects/' . $id . '/auth/password-change', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'onPasswordChange' => true,
]);
$this->assertTrue($response['body']['onPasswordChange']);
return $data;
}
/**
* @depends testCreateProject
*/

View file

@ -1117,7 +1117,7 @@ trait UsersBase
]);
$this->assertEquals(401, $session['headers']['status-code']);
$this->updateProjectOnPasswordChangeProperty(true);
$user = $this->client->call(Client::METHOD_PATCH, '/users/' . $data['userId'] . '/password', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -1129,6 +1129,15 @@ trait UsersBase
$this->assertNotEmpty($user['body']['$id']);
$this->assertNotEmpty($user['body']['password']);
$sessions = $this->client->call(Client::METHOD_GET, '/users/' . $data['userId'] . '/sessions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals($sessions['headers']['status-code'], 200);
$this->assertIsArray($sessions['body']);
$this->assertEmpty($sessions['body']['sessions']);
$session = $this->client->call(Client::METHOD_POST, '/account/sessions/email', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
@ -1138,7 +1147,7 @@ trait UsersBase
]);
$this->assertEquals($session['headers']['status-code'], 201);
$this->updateProjectOnPasswordChangeProperty(false);
return $data;
}