feat: add user verification enforcement for Console project

This commit is contained in:
Harsh Mahajan
2025-09-28 10:45:59 +00:00
parent aa4a58c8aa
commit 0decb1312a
3 changed files with 355 additions and 0 deletions
+38
View File
@@ -325,6 +325,44 @@ App::init()
Authorization::setDefaultStatus(false); // Cancel security segmentation for admin users.
}
// User verification middleware
$globalVerificationEnabled = System::getEnv('_APP_VERIFICATION_REQUIRED', 'disabled') === 'enabled';
$isConsoleProject = $project->getId() === 'console';
if (empty($apiKey) && $globalVerificationEnabled && $isConsoleProject && !$user->isEmpty()) {
$currentPath = $request->getURI();
// Endpoints that must remain accessible
$allowedEndpoints = [
'/v1/account', // User account operations
'/v1/console/variables', // Console configuration
'/v1/health/version', // Health checks
'/v1/account/verification', // Email verification
'/v1/account/verification/phone', // Phone verification
'/v1/account/recovery', // Account recovery
'/v1/account/sessions', // Session management
'/v1/account/tokens', // Token management
'/v1/account/mfa' // Multi-factor authentication
];
$isAllowedEndpoint = false;
foreach ($allowedEndpoints as $allowedEndpoint) {
if (str_starts_with($currentPath, $allowedEndpoint)) {
$isAllowedEndpoint = true;
break;
}
}
if (!$isAllowedEndpoint) {
$emailVerified = $user->getAttribute('emailVerification', false);
$phoneVerified = $user->getAttribute('phoneVerification', false);
if (!$emailVerified && !$phoneVerified) {
throw new Exception(Exception::USER_VERIFICATION_REQUIRED);
}
}
}
$scopes = \array_unique($scopes);
Authorization::setRole($role);
@@ -0,0 +1,108 @@
<?php
namespace Tests\E2E\Services\Projects;
use Tests\E2E\Client;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\SideServer;
use Utopia\Database\Helpers\ID;
class ProjectsVerificationE2ETest extends Scope
{
use ProjectCustom;
use SideServer;
public function testVerificationMiddlewareWithConsoleProject()
{
// Test with console project (should trigger verification check when enabled)
$this->client->setProject('console');
// Test accessing a protected endpoint without verification
// Note: This test assumes verification is enabled via environment variable
$response = $this->client->call('GET', '/users', [], [
'content-type' => 'application/json'
]);
// Should either work (if verification disabled) or require verification
$this->assertContains($response['headers']['status-code'], [200, 401, 403]);
}
public function testVerificationMiddlewareWithRegularProject()
{
// Test with regular project (should not trigger verification check)
$this->client->setProject($this->getProject()['$id']);
// Test accessing a protected endpoint
$response = $this->client->call('GET', '/users', [], [
'content-type' => 'application/json'
]);
// Should work normally for non-console projects
$this->assertContains($response['headers']['status-code'], [200, 401]);
}
public function testAllowedEndpointsWorkWithoutVerification()
{
// Test that allowed endpoints work without verification
$this->client->setProject('console');
$allowedEndpoints = [
'/account',
'/console/variables',
'/health/version'
];
foreach ($allowedEndpoints as $endpoint) {
$response = $this->client->call('GET', $endpoint, [], [
'content-type' => 'application/json'
]);
// Should work regardless of verification status
$this->assertNotEquals(403, $response['headers']['status-code'], "Endpoint $endpoint should be allowed without verification");
}
}
public function testEnvironmentVariableControl()
{
// Test that environment variable controls the verification system
// This test verifies the system respects the _APP_VERIFICATION_REQUIRED setting
$this->client->setProject('console');
// Test with verification potentially enabled
$response = $this->client->call('GET', '/users', [], [
'content-type' => 'application/json'
]);
// Should work or require verification based on environment setting
$this->assertContains($response['headers']['status-code'], [200, 401, 403]);
}
public function testVerificationMiddlewareWithApiKey()
{
// Test that API key authentication bypasses verification
$this->client->setProject('console');
$response = $this->client->call('GET', '/users', [], [
'content-type' => 'application/json',
'x-appwrite-key' => $this->getProject()['apiKey']
]);
// Should work with API key regardless of verification
$this->assertNotEquals(403, $response['headers']['status-code'], "API key should bypass verification");
}
public function testVerificationMiddlewareWithEmptyUser()
{
// Test that empty/unauthenticated users are handled properly
$this->client->setProject('console');
$response = $this->client->call('GET', '/users', [], [
'content-type' => 'application/json'
]);
// Should either work or require authentication, but not verification specifically
$this->assertContains($response['headers']['status-code'], [200, 401, 403]);
}
}
@@ -0,0 +1,209 @@
<?php
namespace Tests\Unit\Network\Validators;
use PHPUnit\Framework\TestCase;
use Utopia\Database\Document;
use Utopia\System\System;
/**
* Verification Test
*
* Tests the verification middleware logic using existing user verification fields
*/
class VerificationTest extends TestCase
{
protected Document $verifiedUser;
protected Document $unverifiedUser;
protected Document $emptyUser;
protected function setUp(): void
{
parent::setUp();
$this->verifiedUser = new Document([
'$id' => 'user1',
'emailVerification' => true,
'phoneVerification' => false,
]);
$this->unverifiedUser = new Document([
'$id' => 'user2',
'emailVerification' => false,
'phoneVerification' => false,
]);
$this->emptyUser = new Document([]);
}
public function testVerificationMiddlewareWithGlobalDisabled()
{
// Mock environment variable
$_ENV['_APP_VERIFICATION_REQUIRED'] = 'disabled';
$verificationRequired = System::getEnv('_APP_VERIFICATION_REQUIRED', 'disabled') === 'enabled';
$this->assertFalse($verificationRequired);
}
public function testVerificationMiddlewareWithGlobalEnabled()
{
// Test the logic directly
$globalVerificationEnabled = 'enabled' === 'enabled';
$this->assertTrue($globalVerificationEnabled);
}
public function testProjectVerificationRequired()
{
$project = new Document([
'$id' => 'console',
'verificationRequired' => true
]);
$projectVerificationRequired = $project->getAttribute('verificationRequired', false);
$this->assertTrue($projectVerificationRequired);
}
public function testProjectVerificationNotRequired()
{
$project = new Document([
'$id' => 'regular-project',
'verificationRequired' => false
]);
$projectVerificationRequired = $project->getAttribute('verificationRequired', false);
$this->assertFalse($projectVerificationRequired);
}
public function testCombinedVerificationLogic()
{
// Test both global and project settings must be true
$globalVerificationEnabled = true;
$projectVerificationRequired = true;
$verificationActive = $globalVerificationEnabled && $projectVerificationRequired;
$this->assertTrue($verificationActive);
}
public function testCombinedVerificationLogicGlobalDisabled()
{
// Test global disabled overrides project setting
$globalVerificationEnabled = false;
$projectVerificationRequired = true;
$verificationActive = $globalVerificationEnabled && $projectVerificationRequired;
$this->assertFalse($verificationActive);
}
public function testCombinedVerificationLogicProjectDisabled()
{
// Test project disabled overrides global setting
$globalVerificationEnabled = true;
$projectVerificationRequired = false;
$verificationActive = $globalVerificationEnabled && $projectVerificationRequired;
$this->assertFalse($verificationActive);
}
public function testAllowedEndpoints()
{
$allowedEndpoints = [
'/v1/account',
'/v1/console/variables',
'/v1/health/version',
'/v1/account/verification',
'/v1/account/verification/phone',
'/v1/account/recovery',
'/v1/account/sessions',
'/v1/account/tokens',
'/v1/account/mfa'
];
$currentPath = '/v1/account/sessions';
$isAllowedEndpoint = false;
foreach ($allowedEndpoints as $allowedEndpoint) {
if (str_starts_with($currentPath, $allowedEndpoint)) {
$isAllowedEndpoint = true;
break;
}
}
$this->assertTrue($isAllowedEndpoint);
}
public function testProtectedEndpoints()
{
$allowedEndpoints = [
'/v1/account',
'/v1/console/variables',
'/v1/health/version',
'/v1/account/verification',
'/v1/account/verification/phone',
'/v1/account/recovery',
'/v1/account/sessions',
'/v1/account/tokens',
'/v1/account/mfa'
];
$currentPath = '/v1/users';
$isAllowedEndpoint = false;
foreach ($allowedEndpoints as $allowedEndpoint) {
if (str_starts_with($currentPath, $allowedEndpoint)) {
$isAllowedEndpoint = true;
break;
}
}
$this->assertFalse($isAllowedEndpoint);
}
public function testVerifiedUserAccess()
{
$emailVerified = $this->verifiedUser->getAttribute('emailVerification', false);
$phoneVerified = $this->verifiedUser->getAttribute('phoneVerification', false);
$this->assertTrue($emailVerified || $phoneVerified);
}
public function testUnverifiedUserAccess()
{
$emailVerified = $this->unverifiedUser->getAttribute('emailVerification', false);
$phoneVerified = $this->unverifiedUser->getAttribute('phoneVerification', false);
$this->assertFalse($emailVerified || $phoneVerified);
}
public function testEmptyUserAccess()
{
$this->assertTrue($this->emptyUser->isEmpty());
}
public function testPhoneVerifiedUser()
{
$phoneVerifiedUser = new Document([
'$id' => 'user3',
'emailVerification' => false,
'phoneVerification' => true,
]);
$emailVerified = $phoneVerifiedUser->getAttribute('emailVerification', false);
$phoneVerified = $phoneVerifiedUser->getAttribute('phoneVerification', false);
$this->assertTrue($emailVerified || $phoneVerified);
}
protected function tearDown(): void
{
// Clean up environment variables
unset($_ENV['_APP_VERIFICATION_REQUIRED']);
parent::tearDown();
}
}