diff --git a/app/cli.php b/app/cli.php index d2c6496dc3..8902aad886 100644 --- a/app/cli.php +++ b/app/cli.php @@ -5,6 +5,9 @@ require_once __DIR__.'/controllers/general.php'; use Utopia\App; use Utopia\CLI\CLI; use Utopia\CLI\Console; +use Utopia\Database\Validator\Authorization; + +Authorization::disable(); $cli = new CLI(); diff --git a/app/config/collections.php b/app/config/collections.php index 35aa37d352..487f893423 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -1054,9 +1054,9 @@ $collections = [ 'size' => 16384, 'signed' => true, 'required' => false, - 'default' => [], - 'array' => true, - 'filters' => ['json'], + 'default' => null, + 'array' => false, + 'filters' => ['subQuerySessions'], ], [ '$id' => 'tokens', @@ -1478,6 +1478,13 @@ $collections = [ 'lengths' => [100, 100], 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC], ], + [ + '$id' => '_key_user', + 'type' => Database::INDEX_KEY, + 'attributes' => ['userId'], + 'lengths' => [Database::LENGTH_KEY], + 'orders' => [Database::ORDER_ASC], + ], ], ], diff --git a/app/config/errors.php b/app/config/errors.php index b7b5c1ca31..8dadcf079c 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -78,6 +78,11 @@ return [ 'description' => 'An internal server error occurred.', 'code' => 500, ], + Exception::GENERAL_PROTOCOL_UNSUPPORTED => [ + 'name' => Exception::GENERAL_PROTOCOL_UNSUPPORTED, + 'description' => 'The request cannot be fulfilled with the current protocol. Please check the value of the _APP_OPTIONS_FORCE_HTTPS environment variable.', + 'code' => 500, + ], /** User Errors */ Exception::USER_COUNT_EXCEEDED => [ diff --git a/app/config/locale/translations/en.json b/app/config/locale/translations/en.json index c00740b130..ab765fe47e 100644 --- a/app/config/locale/translations/en.json +++ b/app/config/locale/translations/en.json @@ -27,6 +27,12 @@ "emails.invitation.footer": "If you are not interested, you can ignore this message.", "emails.invitation.thanks": "Thanks", "emails.invitation.signature": "{{project}} team", + "emails.certificate.subject": "Certificate failure for %s", + "emails.certificate.hello": "Hello", + "emails.certificate.body": "Certificate for your domain '{{domain}}' could not be generated. This is attempt no. {{attempt}}, and the failure was caused by: {{error}}", + "emails.certificate.footer": "Your previous certificate willl be valid for 30 days since the first failure. We highly recommend investigating this case, otherwise your domain will end up without a valid SSL communication.", + "emails.certificate.thanks": "Thanks", + "emails.certificate.signature": "{{project}} team", "locale.country.unknown": "Unknown", "countries.af": "Afghanistan", "countries.ao": "Angola", diff --git a/app/config/providers.php b/app/config/providers.php index 1d364a3fcf..c0acceb0fc 100644 --- a/app/config/providers.php +++ b/app/config/providers.php @@ -241,16 +241,6 @@ return [ // Ordered by ABC. 'beta' => false, 'mock' => false, ], - 'vk' => [ - 'name' => 'VK', - 'developers' => 'https://vk.com/dev', - 'icon' => 'icon-vk', - 'enabled' => true, - 'sandbox' => false, - 'form' => false, - 'beta' => false, - 'mock' => false, - ], 'zoom' => [ 'name' => 'Zoom', 'developers' => 'https://marketplace.zoom.us/docs/guides/auth/oauth/', diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 3462eec2cb..eedc8f6dfe 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -104,7 +104,7 @@ App::post('/v1/account') 'reset' => false, 'name' => $name, 'prefs' => new \stdClass(), - 'sessions' => [], + 'sessions' => null, 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), @@ -167,7 +167,10 @@ App::post('/v1/account/sessions') $email = \strtolower($email); $protocol = $request->getProtocol(); - $profile = $dbForProject->findOne('users', [new Query('deleted', Query::TYPE_EQUAL, [false]), new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address + $profile = $dbForProject->findOne('users', [ + new Query('deleted', Query::TYPE_EQUAL, [false]), + new Query('email', Query::TYPE_EQUAL, [$email])] + ); if (!$profile || !Auth::passwordVerify($password, $profile->getAttribute('password'))) { $audits @@ -208,8 +211,7 @@ App::post('/v1/account/sessions') ->setAttribute('$write', ['user:' . $profile->getId()]) ); - $profile->setAttribute('sessions', $session, Document::SET_TYPE_APPEND); - $profile = $dbForProject->updateDocument('users', $profile->getId(), $profile); + $dbForProject->deleteCachedDocument('users', $profile->getId()); $audits ->setParam('userId', $profile->getId()) @@ -458,13 +460,10 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $current = Auth::sessionVerify($sessions, Auth::$secret); if ($current) { // Delete current session of new one. - foreach ($sessions as $key => $session) {/** @var Document $session */ - if ($current === $session['$id']) { - unset($sessions[$key]); - - $dbForProject->deleteDocument('sessions', $session->getId()); - $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('sessions', $sessions)); - } + $currentDocument = $dbForProject->getDocument('sessions', $current); + if(!$currentDocument->isEmpty()) { + $dbForProject->deleteDocument('sessions', $currentDocument->getId()); + $dbForProject->deleteCachedDocument('users', $user->getId()); } } @@ -476,14 +475,21 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') if ($user === false || $user->isEmpty()) { // No user logged in or with OAuth2 provider ID, create new one or connect with account with same email $name = $oauth2->getUserName($accessToken); $email = $oauth2->getUserEmail($accessToken); + $isVerified = $oauth2->isEmailVerified($accessToken); - $user = $dbForProject->findOne('users', [new Query('deleted', Query::TYPE_EQUAL, [false]), new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address + if ($isVerified === true) { + // Get user by email address + $user = $dbForProject->findOne('users', [ + new Query('deleted', Query::TYPE_EQUAL, [false]), + new Query('email', Query::TYPE_EQUAL, [$email])] + ); + } if ($user === false || $user->isEmpty()) { // Last option -> create the user, generate random password $limit = $project->getAttribute('auths', [])['limit'] ?? 0; if ($limit !== 0) { - $total = $dbForProject->count('users', [ new Query('deleted', Query::TYPE_EQUAL, [false]),], APP_LIMIT_USERS); + $total = $dbForProject->count('users', [new Query('deleted', Query::TYPE_EQUAL, [false])], APP_LIMIT_USERS); if ($total >= $limit) { throw new Exception('Project registration is restricted. Contact your administrator for more information.', 501, Exception::USER_COUNT_EXCEEDED); @@ -497,7 +503,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') '$read' => ['role:all'], '$write' => ['user:' . $userId], 'email' => $email, - 'emailVerification' => true, + 'emailVerification' => $isVerified, 'status' => true, // Email should already be authenticated by OAuth2 provider 'password' => Auth::passwordHash(Auth::passwordGenerator()), 'passwordUpdate' => 0, @@ -505,7 +511,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'reset' => false, 'name' => $name, 'prefs' => new \stdClass(), - 'sessions' => [], + 'sessions' => null, 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), @@ -522,7 +528,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') } // Create session token, verify user account and update OAuth2 ID and Access Token - $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); $secret = Auth::tokenGenerator(); @@ -553,17 +558,18 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') $user ->setAttribute('status', true) - ->setAttribute('sessions', $session, Document::SET_TYPE_APPEND) ; Authorization::setRole('user:' . $user->getId()); + $dbForProject->updateDocument('users', $user->getId(), $user); + $session = $dbForProject->createDocument('sessions', $session ->setAttribute('$read', ['user:' . $user->getId()]) ->setAttribute('$write', ['user:' . $user->getId()]) ); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->deleteCachedDocument('users', $user->getId()); $audits ->setParam('userId', $user->getId()) @@ -679,7 +685,7 @@ App::post('/v1/account/sessions/magic-url') 'registration' => \time(), 'reset' => false, 'prefs' => new \stdClass(), - 'sessions' => [], + 'sessions' => null, 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email]), @@ -824,6 +830,10 @@ App::put('/v1/account/sessions/magic-url') ->setAttribute('$write', ['user:' . $user->getId()]) ); + $dbForProject->deleteCachedDocument('users', $user->getId()); + + $tokens = $user->getAttribute('tokens', []); + /** * We act like we're updating and validating * the recovery token but actually we don't need it anymore. @@ -831,7 +841,10 @@ App::put('/v1/account/sessions/magic-url') $dbForProject->deleteDocument('tokens', $token); $dbForProject->deleteCachedDocument('users', $user->getId()); - $user = $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('sessions', $session, Document::SET_TYPE_APPEND)); + $user + ->setAttribute('emailVerification', true); + + $user = $dbForProject->updateDocument('users', $user->getId(), $user); if (false === $user) { throw new Exception('Failed saving user to DB', 500, Exception::GENERAL_SERVER_ERROR); @@ -938,7 +951,7 @@ App::post('/v1/account/sessions/anonymous') 'reset' => false, 'name' => null, 'prefs' => new \stdClass(), - 'sessions' => [], + 'sessions' => null, 'tokens' => null, 'memberships' => null, 'search' => $userId, @@ -974,8 +987,7 @@ App::post('/v1/account/sessions/anonymous') ->setAttribute('$write', ['user:' . $user->getId()]) ); - $user = $dbForProject->updateDocument('users', $user->getId(), - $user->setAttribute('sessions', $session, Document::SET_TYPE_APPEND)); + $dbForProject->deleteCachedDocument('users', $user->getId()); $audits ->setParam('userId', $user->getId()) @@ -1026,16 +1038,17 @@ App::post('/v1/account/jwt') ->label('abuse-key', 'url:{url},userId:{userId}') ->inject('response') ->inject('user') - ->action(function ($response, $user) { + ->inject('dbForProject') + ->action(function ($response, $user, $dbForProject) { /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Database\Document $user */ + /** @var Utopia\Database\Database $dbForProject */ + $sessions = $user->getAttribute('sessions', []); $current = new Document(); - foreach ($sessions as $session) { - /** @var Utopia\Database\Document $session */ - + foreach ($sessions as $session) { /** @var Utopia\Database\Document $session */ if ($session->getAttribute('secret') == Auth::hash(Auth::$secret)) { // If current session delete the cookies too $current = $session; } @@ -1619,8 +1632,8 @@ App::delete('/v1/account/sessions/:sessionId') ->addCookie(Auth::$cookieName, '', \time() - 3600, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) ; } - - $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('sessions', $sessions)); + + $dbForProject->deleteCachedDocument('users', $user->getId()); $events ->setParam('eventData', $response->output($session, Response::MODEL_SESSION)) @@ -1714,8 +1727,7 @@ App::patch('/v1/account/sessions/:sessionId') $dbForProject->updateDocument('sessions', $sessionId, $session); - $user->setAttribute("sessions", $sessions); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); + $dbForProject->deleteCachedDocument('users', $user->getId()); $audits ->setParam('userId', $user->getId()) @@ -1801,7 +1813,7 @@ App::delete('/v1/account/sessions') } } - $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('sessions', [])); + $dbForProject->deleteCachedDocument('users', $user->getId()); $numOfSessions = count($sessions); @@ -1864,7 +1876,11 @@ App::post('/v1/account/recovery') $isAppUser = Auth::isAppUser($roles); $email = \strtolower($email); - $profile = $dbForProject->findOne('users', [new Query('deleted', Query::TYPE_EQUAL, [false]), new Query('email', Query::TYPE_EQUAL, [$email])]); // Get user by email address + + $profile = $dbForProject->findOne('users', [ + new Query('deleted', Query::TYPE_EQUAL, [false]), + new Query('email', Query::TYPE_EQUAL, [$email]) + ]); if (!$profile) { throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 38e3fc31c5..a9267da138 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -23,7 +23,7 @@ use Utopia\Registry\Registry; use Appwrite\Extend\Exception; use Utopia\Validator\ArrayList; use Utopia\Validator\Boolean; -use Utopia\Validator\Integer; +use Utopia\Validator\Hostname; use Utopia\Validator\Range; use Utopia\Validator\Text; use Utopia\Validator\WhiteList; @@ -972,6 +972,14 @@ App::post('/v1/projects/:projectId/platforms') ->inject('dbForConsole') ->action(function (string $projectId, string $type, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForConsole) { + // Ensure hostname has proper structure (no port, protocol..) + if(!empty($hostname)) { + $validator = new Hostname(); + if (!is_null($hostname) && !$validator->isValid($hostname)) { + throw new Exception($validator->getDescription(), 400, Exception::ATTRIBUTE_VALUE_INVALID); + } + } + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1085,6 +1093,14 @@ App::put('/v1/projects/:projectId/platforms/:platformId') ->inject('dbForConsole') ->action(function (string $projectId, string $platformId, string $name, string $key, string $store, string $hostname, Response $response, Database $dbForConsole) { + // Ensure hostname has proper structure (no port, protocol..) + if(!empty($hostname)) { + $validator = new Hostname(); + if (!is_null($hostname) && !$validator->isValid($hostname)) { + throw new Exception($validator->getDescription(), 400, Exception::ATTRIBUTE_VALUE_INVALID); + } + } + $project = $dbForConsole->getDocument('projects', $projectId); if ($project->isEmpty()) { @@ -1331,8 +1347,7 @@ App::patch('/v1/projects/:projectId/domains/:domainId/verification') $dbForConsole->deleteCachedDocument('projects', $project->getId()); // Issue a TLS certificate when domain is verified - Resque::enqueue('v1-certificates', 'CertificatesV1', [ - 'document' => $domain->getArrayCopy(), + Resque::enqueue(Event::CERTIFICATES_QUEUE_NAME, Event::CERTIFICATES_CLASS_NAME, [ 'domain' => $domain->getAttribute('domain'), ]); diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 9ad25bda69..91727bb30b 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -57,7 +57,7 @@ App::post('/v1/storage/buckets') ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->param('enabled', true, new Boolean(true), 'Is bucket enabled?', true) - ->param('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0), new Integer(), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self-hosted setups you can change the max limit by changing the `_APP_STORAGE_LIMIT` environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true) + ->param('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0), new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human(App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self-hosted setups you can change the max limit by changing the `_APP_STORAGE_LIMIT` environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true) ->param('allowedFileExtensions', [], new ArrayList(new Text(64)), 'Allowed file extensions', true) ->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true) ->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true) @@ -223,7 +223,7 @@ App::put('/v1/storage/buckets/:bucketId') ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true) ->param('enabled', true, new Boolean(true), 'Is bucket enabled?', true) - ->param('maximumFileSize', null, new Integer(), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human((int)App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self hosted version you can change the limit by changing _APP_STORAGE_LIMIT environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true) + ->param('maximumFileSize', null, new Range(1, (int) App::getEnv('_APP_STORAGE_LIMIT', 0)), 'Maximum file size allowed in bytes. Maximum allowed value is ' . Storage::human((int)App::getEnv('_APP_STORAGE_LIMIT', 0), 0) . '. For self hosted version you can change the limit by changing _APP_STORAGE_LIMIT environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true) ->param('allowedFileExtensions', [], new ArrayList(new Text(64)), 'Allowed file extensions', true) ->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true) ->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true) @@ -397,7 +397,7 @@ App::post('/v1/storage/buckets/:bucketId/files') $maximumFileSize = $bucket->getAttribute('maximumFileSize', 0); if ($maximumFileSize > (int) App::getEnv('_APP_STORAGE_LIMIT', 0)) { - throw new Exception('Error bucket maximum file size is larger than _APP_STORAGE_LIMIT', 500, Exception::GENERAL_SERVER_ERROR); + throw new Exception('Maximum bucket file size is larger than _APP_STORAGE_LIMIT', 500, Exception::GENERAL_SERVER_ERROR); } $file = $request->getFiles('file'); diff --git a/app/controllers/api/teams.php b/app/controllers/api/teams.php index 501096e349..a6bb58ba3d 100644 --- a/app/controllers/api/teams.php +++ b/app/controllers/api/teams.php @@ -341,7 +341,7 @@ App::post('/v1/teams/:teamId/memberships') 'reset' => false, 'name' => $name, 'prefs' => new \stdClass(), - 'sessions' => [], + 'sessions' => null, 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), @@ -708,11 +708,10 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status') ->setAttribute('$write', ['user:'.$user->getId()]) ); - $user->setAttribute('sessions', $session, Document::SET_TYPE_APPEND); + $dbForProject->deleteCachedDocument('users', $user->getId()); Authorization::setRole('user:'.$userId); - $user = $dbForProject->updateDocument('users', $user->getId(), $user); $membership = $dbForProject->updateDocument('memberships', $membership->getId(), $membership); $dbForProject->deleteCachedDocument('users', $user->getId()); diff --git a/app/controllers/api/users.php b/app/controllers/api/users.php index d4b2109798..2f33098bcb 100644 --- a/app/controllers/api/users.php +++ b/app/controllers/api/users.php @@ -63,7 +63,7 @@ App::post('/v1/users') 'reset' => false, 'name' => $name, 'prefs' => new \stdClass(), - 'sessions' => [], + 'sessions' => null, 'tokens' => null, 'memberships' => null, 'search' => implode(' ', [$userId, $email, $name]), @@ -632,25 +632,20 @@ App::delete('/v1/users/:userId/sessions/:sessionId') throw new Exception('User not found', 404, Exception::USER_NOT_FOUND); } - $sessions = $user->getAttribute('sessions', []); + $session = $dbForProject->getDocument('sessions', $sessionId); - foreach ($sessions as $key => $session) { /** @var Document $session */ - - if ($sessionId == $session->getId()) { - unset($sessions[$key]); - - $dbForProject->deleteDocument('sessions', $session->getId()); - - $user->setAttribute('sessions', $sessions); - - $events - ->setParam('eventData', $response->output($user, Response::MODEL_USER)) - ; - - $dbForProject->updateDocument('users', $user->getId(), $user); - } + if($session->isEmpty()) { + throw new Exception('User not found', 404, Exception::USER_SESSION_NOT_FOUND); } + $dbForProject->deleteDocument('sessions', $session->getId()); + + $dbForProject->deleteCachedDocument('users', $user->getId()); + + $events + ->setParam('eventData', $response->output($user, Response::MODEL_USER)) + ; + $usage ->setParam('users.update', 1) ->setParam('users.sessions.delete', 1) @@ -693,7 +688,7 @@ App::delete('/v1/users/:userId/sessions') $dbForProject->deleteDocument('sessions', $session->getId()); } - $dbForProject->updateDocument('users', $user->getId(), $user->setAttribute('sessions', [])); + $dbForProject->deleteCachedDocument('users', $user->getId()); $events ->setParam('eventData', $response->output($user, Response::MODEL_USER)) diff --git a/app/controllers/general.php b/app/controllers/general.php index eacf926955..b9c45443b6 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -20,6 +20,7 @@ use Utopia\CLI\Console; use Utopia\Database\Document; use Utopia\Database\Query; use Utopia\Database\Validator\Authorization; +use Utopia\Validator\Hostname; use Appwrite\Utopia\Request\Filters\V12 as RequestV12; use Appwrite\Utopia\Request\Filters\V13 as RequestV13; use Utopia\Validator\Text; @@ -99,14 +100,11 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons ]); $domainDocument = $dbForConsole->createDocument('domains', $domainDocument); - + Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...'); - + Resque::enqueue(Event::CERTIFICATES_QUEUE_NAME, Event::CERTIFICATES_CLASS_NAME, [ - 'document' => $domainDocument, - 'domain' => $domain->get(), - 'validateTarget' => false, - 'validateCNAME' => false, + 'domain' => $domain->get() ]); } } @@ -135,8 +133,13 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons $protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME); $port = \parse_url($request->getOrigin($referrer), PHP_URL_PORT); - $refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()).'://'.((\in_array($origin, $clients)) - ? $origin : 'localhost').(!empty($port) ? ':'.$port : ''); + $refDomainOrigin = 'localhost'; + $validator = new Hostname($clients); + if ($validator->isValid($origin)) { + $refDomainOrigin = $origin; + } + + $refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $refDomainOrigin . (!empty($port) ? ':' . $port : ''); $refDomain = (!$route->getLabel('origin', false)) // This route is publicly accessible ? $refDomain @@ -185,6 +188,10 @@ App::init(function ($utopia, $request, $response, $console, $project, $dbForCons */ if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS if ($request->getProtocol() !== 'https') { + if($request->getMethod() !== Request::METHOD_GET) { + throw new Exception('Method unsupported over HTTP.', 500, Exception::GENERAL_PROTOCOL_UNSUPPORTED); + } + return $response->redirect('https://'.$request->getHostname().$request->getURI()); } diff --git a/app/init.php b/app/init.php index e693906167..269d860fe6 100644 --- a/app/init.php +++ b/app/init.php @@ -127,6 +127,7 @@ const MAIL_TYPE_VERIFICATION = 'verification'; const MAIL_TYPE_MAGIC_SESSION = 'magicSession'; const MAIL_TYPE_RECOVERY = 'recovery'; const MAIL_TYPE_INVITATION = 'invitation'; +const MAIL_TYPE_CERTIFICATE = 'certificate'; // Auth Types const APP_AUTH_TYPE_SESSION = 'Session'; const APP_AUTH_TYPE_JWT = 'JWT'; @@ -301,6 +302,19 @@ Database::addFilter('subQueryWebhooks', } ); +Database::addFilter('subQuerySessions', + function($value) { + return null; + }, + function($value, Document $document, Database $database) { + $sessions = Authorization::skip(fn () => $database->find('sessions', [ + new Query('userId', Query::TYPE_EQUAL, [$document->getId()]) + ], $database->getIndexLimit(), 0, [])); + + return $sessions; + } +); + Database::addFilter('subQueryTokens', function($value) { return null; diff --git a/app/tasks/maintenance.php b/app/tasks/maintenance.php index a6f37ff71b..056ee59fcd 100644 --- a/app/tasks/maintenance.php +++ b/app/tasks/maintenance.php @@ -1,10 +1,42 @@ get('cache'))); + $database = new Database(new MariaDB($register->get('db')), $cache); + $database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); + $database->setNamespace('_console'); // Main DB + break; // leave loop if successful + } catch(\Exception $e) { + Console::warning("Database not ready. Retrying connection ({$attempts})..."); + if ($attempts >= DATABASE_RECONNECT_MAX_ATTEMPTS) { + throw new \Exception('Failed to connect to database: '. $e->getMessage()); + } + sleep(DATABASE_RECONNECT_SLEEP); + } + } while ($attempts < DATABASE_RECONNECT_MAX_ATTEMPTS); + + return $database; +} $cli ->task('maintenance') @@ -54,6 +86,29 @@ $cli ]); } + function renewCertificates($dbForConsole) + { + $time = date('d-m-Y H:i:s', time()); + /** @var Utopia\Database\Database $dbForConsole */ + + $certificates = $dbForConsole->find('certificates', [ + new Query('attempts', Query::TYPE_LESSEREQUAL, [5]), // Maximum 5 attempts + new Query('renewDate', Query::TYPE_LESSEREQUAL, [\time()]) // includes 60 days cooldown (we have 30 days to renew) + ], 200); // Limit 200 comes from LetsEncrypt (300 orders per 3 hours, keeping some for new domains) + + if(\count($certificates) > 0) { + Console::info("[{$time}] Found " . \count($certificates) . " certificates for renewal, scheduling jobs."); + + foreach ($certificates as $certificate) { + Resque::enqueue(Event::CERTIFICATES_QUEUE_NAME, Event::CERTIFICATES_CLASS_NAME, [ + 'domain' => $certificate->getAttribute('domain'), + ]); + } + } else { + Console::info("[{$time}] No certificates for renewal."); + } + } + // # of days in seconds (1 day = 86400s) $interval = (int) App::getEnv('_APP_MAINTENANCE_INTERVAL', '86400'); $executionLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', '1209600'); @@ -62,13 +117,17 @@ $cli $usageStatsRetention30m = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_30M', '129600');//36 hours $usageStatsRetention1d = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_1D', '8640000'); // 100 days - Console::loop(function() use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) { + Console::loop(function() use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) { + $database = getConsoleDB(); + $time = date('d-m-Y H:i:s', time()); - Console::info("[{$time}] Notifying deletes workers every {$interval} seconds"); + Console::info("[{$time}] Notifying workers with maintenance tasks every {$interval} seconds"); notifyDeleteExecutionLogs($executionLogsRetention); notifyDeleteAbuseLogs($abuseLogsRetention); notifyDeleteAuditLogs($auditLogRetention); notifyDeleteUsageStats($usageStatsRetention30m, $usageStatsRetention1d); notifyDeleteConnections(); + + renewCertificates($database); }, $interval); }); \ No newline at end of file diff --git a/app/tasks/ssl.php b/app/tasks/ssl.php index 2c32324fa3..743be92a96 100644 --- a/app/tasks/ssl.php +++ b/app/tasks/ssl.php @@ -2,22 +2,21 @@ global $cli; +use Appwrite\Event\Event; use Utopia\App; use Utopia\CLI\Console; +use Utopia\Validator\Hostname; $cli ->task('ssl') ->desc('Validate server certificates') - ->action(function () { - $domain = App::getEnv('_APP_DOMAIN', ''); + ->param('domain', App::getEnv('_APP_DOMAIN', ''), new Hostname(), 'Domain to generate certificate for. If empty, main domain will be used.', true) + ->action(function ($domain) { + Console::success('Scheduling a job to issue a TLS certificate for domain: ' . $domain); - Console::log('Issue a TLS certificate for master domain ('.$domain.') in 30 seconds. - Make sure your domain points to your server or restart to try again.'); - - ResqueScheduler::enqueueAt(\time() + 30, 'v1-certificates', 'CertificatesV1', [ - 'document' => [], + // Scheduje a job + Resque::enqueue(Event::CERTIFICATES_QUEUE_NAME, Event::CERTIFICATES_CLASS_NAME, [ 'domain' => $domain, - 'validateTarget' => false, - 'validateCNAME' => false, + 'skipCheck' => true ]); }); \ No newline at end of file diff --git a/app/views/console/home/index.phtml b/app/views/console/home/index.phtml index 028e7f3b79..82be016b61 100644 --- a/app/views/console/home/index.phtml +++ b/app/views/console/home/index.phtml @@ -299,8 +299,9 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true); - - + + +
You can use * to allow wildcard hostnames or subdomains.
Next Steps
@@ -329,7 +330,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true); data-success="alert,trigger" data-success-param-alert-text="Updated platform successfully" data-success-param-trigger-events="projects.updatePlatform" - data-failure="alert" + data-failure="alert,trigger" + data-failure-param-trigger-events="projects.updatePlatform" data-failure-param-alert-text="Failed to update platform" data-failure-param-alert-classname="error"> @@ -340,7 +342,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true); - + +
You can use * to allow wildcard hostnames or subdomains.

@@ -714,7 +717,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true); data-success="alert,trigger" data-success-param-alert-text="Updated platform successfully" data-success-param-trigger-events="projects.updatePlatform" - data-failure="alert" + data-failure="alert,trigger" + data-failure-param-trigger-events="projects.updatePlatform" data-failure-param-alert-text="Failed to update platform" data-failure-param-alert-classname="error"> @@ -746,7 +750,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true); data-success="alert,trigger" data-success-param-alert-text="Updated platform successfully" data-success-param-trigger-events="projects.updatePlatform" - data-failure="alert" + data-failure="alert,trigger" + data-failure-param-trigger-events="projects.updatePlatform" data-failure-param-alert-text="Failed to update platform" data-failure-param-alert-classname="error"> @@ -777,7 +782,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true); data-success="alert,trigger" data-success-param-alert-text="Updated platform successfully" data-success-param-trigger-events="projects.updatePlatform" - data-failure="alert" + data-failure="alert,trigger" + data-failure-param-trigger-events="projects.updatePlatform" data-failure-param-alert-text="Failed to update platform" data-failure-param-alert-classname="error"> @@ -808,7 +814,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true); data-success="alert,trigger" data-success-param-alert-text="Updated platform successfully" data-success-param-trigger-events="projects.updatePlatform" - data-failure="alert" + data-failure="alert,trigger" + data-failure-param-trigger-events="projects.updatePlatform" data-failure-param-alert-text="Failed to update platform" data-failure-param-alert-classname="error"> @@ -841,7 +848,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true); data-success="alert,trigger" data-success-param-alert-text="Updated platform successfully" data-success-param-trigger-events="projects.updatePlatform" - data-failure="alert" + data-failure="alert,trigger" + data-failure-param-trigger-events="projects.updatePlatform" data-failure-param-alert-text="Failed to update platform" data-failure-param-alert-classname="error"> @@ -873,7 +881,8 @@ $usageStatsEnabled = $this->getParam('usageStatsEnabled', true); data-success="alert,trigger" data-success-param-alert-text="Updated platform successfully" data-success-param-trigger-events="projects.updatePlatform" - data-failure="alert" + data-failure="alert,trigger" + data-failure-param-trigger-events="projects.updatePlatform" data-failure-param-alert-text="Failed to update platform" data-failure-param-alert-classname="error"> diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 7fc5b2b727..c6f7726058 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -342,6 +342,7 @@ services: environment: - _APP_ENV - _APP_OPENSSL_KEY_V1 + - _APP_DOMAIN - _APP_DOMAIN_TARGET - _APP_SYSTEM_SECURITY_EMAIL_ADDRESS - _APP_REDIS_HOST @@ -473,6 +474,8 @@ services: environment: - _APP_ENV - _APP_OPENSSL_KEY_V1 + - _APP_DOMAIN + - _APP_DOMAIN_TARGET - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER diff --git a/app/workers/certificates.php b/app/workers/certificates.php index 9f9ce33dbd..f932e4b8f8 100644 --- a/app/workers/certificates.php +++ b/app/workers/certificates.php @@ -1,9 +1,11 @@ getConsoleDB(); - - /** - * 1. Get new domain document - DONE - * 1.1. Validate domain is valid, public suffix is known and CNAME records are verified - DONE - * 2. Check if a certificate already exists - DONE - * 3. Check if certificate is about to expire, if not - skip it - * 3.1. Create / renew certificate - * 3.2. Update loadblancer - * 3.3. Update database (domains, change date, expiry) - * 3.4. Set retry on failure - * 3.5. Schedule to renew certificate in 60 days - */ - Authorization::disable(); - // Args - $document = $this->args['document']; - $domain = $this->args['domain']; + $this->dbForConsole = $this->getConsoleDB(); - // Validation Args - $validateTarget = $this->args['validateTarget'] ?? true; - $validateCNAME = $this->args['validateCNAME'] ?? true; + /** + * 1. Read arguments and validate domain + * 2. Get main domain + * 3. Validate CNAME DNS if parameter is not main domain (meaning it's custom domain) + * 4. Validate security email. Cannot be empty, required by LetsEncrypt + * 5. Validate renew date with certificate file, unless requested to skip by parameter + * 6. Issue a certificate using certbot CLI + * 7. Update 'log' attribute on certificate document with Certbot message + * 8. Create storage folder for certificate, if not ready already + * 9. Move certificates from Certbot location to our Storage + * 10. Create/Update our Storage with new Traefik config with new certificate paths + * 11. Read certificate file and update 'renewDate' on certificate document + * 12. Update 'issueDate' and 'attempts' on certificate + * + * If at any point unexpected error occurs, program stops without applying changes to document, and error is thrown into worker + * + * If code stops with expected error: + * 1. 'log' attribute on document is updated with error message + * 2. 'attempts' amount is increased + * 3. Console log is shown + * 4. Email is sent to security email + * + * Unless unexpected error occurs, at the end, we: + * 1. Update 'updated' attribute on document + * 2. Save document to database + * 3. Update all domains documents with current certificate ID + * + * Note: Renewals are checked and scheduled from maintenence worker + */ - // Options - $domain = new Domain((!empty($domain)) ? $domain : ''); - $expiry = 60 * 60 * 24 * 30 * 2; // 60 days - $safety = 60 * 60; // 1 hour - $renew = (\time() + $expiry); + try { + // Read arguments + $domain = $this->args['domain']; // String of domain (hostname) + $skipCheck = $this->args['skipCheck'] ?? false; // If true, we won't double-check expiry from cert file - if (empty($domain->get())) { - throw new Exception('Missing domain'); - } + $domain = new Domain((!empty($domain)) ? $domain : ''); - if (!$domain->isKnown() || $domain->isTest()) { - throw new Exception('Unknown public suffix for domain'); - } - - if ($validateTarget) { - $target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', '')); - - if(!$target->isKnown() || $target->isTest()) { - throw new Exception('Unreachable CNAME target ('.$target->get().'), please use a domain with a public suffix.'); - } - } - - if ($validateCNAME) { - $validator = new CNAME($target->get()); // Verify Domain with DNS records - - if(!$validator->isValid($domain->get())) { - throw new Exception('Failed to verify domain DNS records'); - } - } - - $certificate = $dbForConsole->findOne('certificates', [ - new Query('domain', QUERY::TYPE_EQUAL, [$domain->get()]) - ]); - - // $condition = ($certificate - // && $certificate instanceof Document - // && isset($certificate['issueDate']) - // && (($certificate['issueDate'] + ($expiry)) > time())) ? 'true' : 'false'; - - // throw new Exception('cert issued at'.date('d.m.Y H:i', $certificate['issueDate']).' | renew date is: '.date('d.m.Y H:i', ($certificate['issueDate'] + ($expiry))).' | condition is '.$condition); - - $certificate = (!empty($certificate) && $certificate instanceof $certificate) ? $certificate->getArrayCopy() : []; - - if ( - !empty($certificate) - && isset($certificate['issueDate']) - && (($certificate['issueDate'] + ($expiry)) > \time()) - ) { // Check last issue time - throw new Exception('Renew isn\'t required'); - } - - $staging = (App::isProduction()) ? '' : ' --dry-run'; - $email = App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'); - - if (empty($email)) { - throw new Exception('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate'); - } - - $stdout = ''; - $stderr = ''; - - $exit = Console::execute("certbot certonly --webroot --noninteractive --agree-tos{$staging}" - . " --email " . $email - . " -w " . APP_STORAGE_CERTIFICATES - . " -d {$domain->get()}", '', $stdout, $stderr); - - if ($exit !== 0) { - throw new Exception('Failed to issue a certificate with message: ' . $stderr); - } - - $path = APP_STORAGE_CERTIFICATES . '/' . $domain->get(); - - if (!\is_readable($path)) { - if (!\mkdir($path, 0755, true)) { - throw new Exception('Failed to create path...'); - } - } - - if(!@\rename('/etc/letsencrypt/live/'.$domain->get().'/cert.pem', APP_STORAGE_CERTIFICATES.'/'.$domain->get().'/cert.pem')) { - throw new Exception('Failed to rename certificate cert.pem: '.\json_encode($stdout)); - } - - if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/chain.pem')) { - throw new Exception('Failed to rename certificate chain.pem: ' . \json_encode($stdout)); - } - - if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/fullchain.pem')) { - throw new Exception('Failed to rename certificate fullchain.pem: ' . \json_encode($stdout)); - } - - if (!@\rename('/etc/letsencrypt/live/' . $domain->get() . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain->get() . '/privkey.pem')) { - throw new Exception('Failed to rename certificate privkey.pem: ' . \json_encode($stdout)); - } - - $certificate = new Document(\array_merge($certificate, [ - 'domain' => $domain->get(), - 'issueDate' => \time(), - 'renewDate' => $renew, - 'attempts' => 0, - 'log' => \json_encode($stdout), - ])); - - $certificate = $dbForConsole->createDocument('certificates', $certificate); - - if (!$certificate) { - throw new Exception('Failed saving certificate to DB'); - } - - if(!empty($document)) { - $certificate = new Document(\array_merge($document, [ - 'updated' => \time(), - 'certificateId' => $certificate->getId(), - ])); - - $certificate = $dbForConsole->updateDocument('domains', $certificate->getId(), $certificate); + // Get current certificate + $certificate = $this->dbForConsole->findOne('certificates', [ new Query('domain', Query::TYPE_EQUAL, [$domain->get()]) ]); + // If we don't have certificate for domain yet, let's create new document. At the end we save it if(!$certificate) { - throw new Exception('Failed saving domain to DB'); + $certificate = new Document(); + $certificate->setAttribute('domain', $domain->get()); } + + // Email for alerts is required by LetsEncrypt + $email = App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'); + if (empty($email)) { + throw new Exception('You must set a valid security email address (_APP_SYSTEM_SECURITY_EMAIL_ADDRESS) to issue an SSL certificate.'); + } + + // Validate domain and DNS records. Skip if job is forced + if(!$skipCheck) { + $mainDomain = $this->getMainDomain(); + $isMainDomain = !isset($mainDomain) || $domain->get() === $mainDomain; + $this->validateDomain($domain, $isMainDomain); + } + + // If certificate exists already, double-check expiry date. Skip if job is forced + if(!$skipCheck && !$this->isRenewRequired($domain->get())) { + throw new Exception('Renew isn\'t required.'); + } + + // Generate certificate files using Let's Encrypt + $letsEncryptData = $this->issueCertificate($domain->get(), $email); + + // Command succeeded, store all data into document + // We store stderr too, because it may include warnings + $certificate->setAttribute('log', \json_encode([ + 'stdout' => $letsEncryptData['stdout'], + 'stderr' => $letsEncryptData['stderr'], + ])); + + // Give certificates to Traefik + $this->applyCertificateFiles($domain->get(), $letsEncryptData); + + // Update certificate info stored in database + $certificate->setAttribute('renewDate', $this->getRenewDate($domain->get())); + $certificate->setAttribute('attempts', 0); + $certificate->setAttribute('issueDate', \time()); + } catch(Throwable $e) { + // Set exception as log in certificate document + $certificate->setAttribute('log', $e->getMessage()); + + // Increase attempts count + $attempts = $certificate->getAttribute('attempts', 0) + 1; + $certificate->setAttribute('attempts', $attempts); + + // Send email to security email + $this->notifyError($domain->get(), $e->getMessage(), $attempts); + } finally { + // All actions result in new updatedAt date + $certificate->setAttribute('updated', \time()); + + // Save all changes we made to certificate document into database + $this->saveCertificateDocument($domain->get(), $certificate); + + Authorization::reset(); } - - $config = -"tls: - certificates: - - certFile: /storage/certificates/{$domain->get()}/fullchain.pem - keyFile: /storage/certificates/{$domain->get()}/privkey.pem"; - - if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain->get() . '.yml', $config)) { - throw new Exception('Failed to save SSL configuration'); - } - - ResqueScheduler::enqueueAt($renew + $safety, 'v1-certificates', 'CertificatesV1', [ - 'document' => [], - 'domain' => $domain->get(), - 'validateTarget' => $validateTarget, - 'validateCNAME' => $validateCNAME, - ]); // Async task rescheduale - - Authorization::reset(); } public function shutdown(): void { } + + /** + * Save certificate data into database. + * + * @param string $domain Domain name that certificate is for + * @param Document $certificate Certificate document that we need to save + * + * @return void + */ + private function saveCertificateDocument(string $domain, Document $certificate): void { + // Check if update or insert required + $certificateDocument = $this->dbForConsole->findOne('certificates', [ new Query('domain', Query::TYPE_EQUAL, [$domain]) ]); + if (!empty($certificateDocument) && !$certificateDocument->isEmpty()) { + // Merge new data with current data + $certificate = new Document(\array_merge($certificateDocument->getArrayCopy(), $certificate->getArrayCopy())); + + $certificate = $this->dbForConsole->updateDocument('certificates', $certificate->getId(), $certificate); + } else { + $certificate = $this->dbForConsole->createDocument('certificates', $certificate); + } + + $certificateId = $certificate->getId(); + $this->updateDomainDocuments($certificateId, $domain); + } + + /** + * Get main domain. Needed as we do different checks for main and non-main domains. + * + * @return null|string Returns main domain. If null, there is no main domain yet. + */ + private function getMainDomain(): ?string { + if (!empty(App::getEnv('_APP_DOMAIN', ''))) { + return App::getEnv('_APP_DOMAIN', ''); + } else { + $domainDocument = $this->dbForConsole->findOne('domains', [], 0, ['_id'], ['ASC']); + if($domainDocument) { + return $domainDocument->getAttribute('domain'); + } + } + + return null; + } + + /** + * Internal domain validation functionality to prevent unnecessary attempts failed from Let's Encrypt side. We check: + * - Domain needs to be public and valid (prevents NFT domains that are not supported by Let's Encrypt) + * - Domain must have proper DNS record + * + * @param Domain $domain Domain which we validate + * @param bool $isMainDomain In case of master domain, we look for different DNS configurations + * + * @return void + */ + private function validateDomain(Domain $domain, bool $isMainDomain): void { + if (empty($domain->get())) { + throw new Exception('Missing certificate domain.'); + } + + if (!$domain->isKnown() || $domain->isTest()) { + throw new Exception('Unknown public suffix for domain.'); + } + + if (!$isMainDomain) { + // TODO: Would be awesome to also support A/AAAA records here. Maybe dry run? + + // Validate if domain target is properly configured + $target = new Domain(App::getEnv('_APP_DOMAIN_TARGET', '')); + + if (!$target->isKnown() || $target->isTest()) { + throw new Exception('Unreachable CNAME target ('.$target->get().'), please use a domain with a public suffix.'); + } + + // Verify domain with DNS records + $validator = new CNAME($target->get()); + if (!$validator->isValid($domain->get())) { + throw new Exception('Failed to verify domain DNS records.'); + } + } else { + // Main domain validation + // TODO: Would be awesome to check A/AAAA record here. Maybe dry run? + } + } + + /** + * Reads expiry date of certificate from file and decides if renewal is required or not. + * + * @param string $domain Domain for which we check certificate file + * + * @return bool True, if certificate needs to be renewed + */ + private function isRenewRequired(string $domain): bool { + $certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem'; + if (\file_exists($certPath)) { + $validTo = null; + + $certData = openssl_x509_parse(file_get_contents($certPath)); + $validTo = $certData['validTo_time_t'] ?? 0; + + if (empty($validTo)) { + throw new Exception('Unable to read certificate file (cert.pem).'); + } + + // LetsEncrypt allows renewal 30 days before expiry + $expiryInAdvance = (60*60*24*30); + if ($validTo - $expiryInAdvance > \time()) { + return false; + } + } + + return true; + } + + /** + * LetsEncrypt communication to issue certificate (using certbot CLI) + * + * @param string $domain Domain to generate certificate for + * + * @return array Named array with keys 'stdout' and 'stderr', both string + */ + private function issueCertificate(string $domain, string $email): array { + $staging = (App::isProduction()) ? '' : ' --dry-run'; + + $stdout = ''; + $stderr = ''; + + $staging = (App::isProduction()) ? '' : ' --dry-run'; + $exit = Console::execute("certbot certonly --webroot --noninteractive --agree-tos{$staging}" + . " --email " . $email + . " -w " . APP_STORAGE_CERTIFICATES + . " -d {$domain}", '', $stdout, $stderr); + + // Unexpected error, usually 5XX, API limits, ... + if ($exit !== 0) { + throw new Exception('Failed to issue a certificate with message: ' . $stderr); + } + + return [ + 'stdout' => $stdout, + 'stderr' => $stderr + ]; + } + + /** + * Read new renew date from certificate file generated by Let's Encrypt + * + * @param string $domain Domain which certificate was generated for + * + * @return int + */ + private function getRenewDate(string $domain): int { + $certPath = APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem'; + $certData = openssl_x509_parse(file_get_contents($certPath)); + $validTo = $certData['validTo_time_t'] ?? 0; + $expiryInAdvance = (60*60*24*30); // 30 days + return $validTo - $expiryInAdvance; + } + + /** + * Method to take files from Let's Encrypt, and put it into Traefik. + * + * @param string $domain Domain which certificate was generated for + * @param array $letsEncryptData Let's Encrypt logs to use for additional info when throwing error + * + * @return void + */ + private function applyCertificateFiles(string $domain, array $letsEncryptData): void { + // Prepare folder in storage for domain + $path = APP_STORAGE_CERTIFICATES . '/' . $domain; + if (!\is_readable($path)) { + if (!\mkdir($path, 0755, true)) { + throw new Exception('Failed to create path for certificate.'); + } + } + + // Move generated files from certbot into our storage + if(!@\rename('/etc/letsencrypt/live/'.$domain.'/cert.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/cert.pem')) { + throw new Exception('Failed to rename certificate cert.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + } + + if (!@\rename('/etc/letsencrypt/live/' . $domain . '/chain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/chain.pem')) { + throw new Exception('Failed to rename certificate chain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + } + + if (!@\rename('/etc/letsencrypt/live/' . $domain . '/fullchain.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/fullchain.pem')) { + throw new Exception('Failed to rename certificate fullchain.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + } + + if (!@\rename('/etc/letsencrypt/live/' . $domain . '/privkey.pem', APP_STORAGE_CERTIFICATES . '/' . $domain . '/privkey.pem')) { + throw new Exception('Failed to rename certificate privkey.pem. Let\'s Encrypt log: ' . $letsEncryptData['stderr'] . ' ; ' . $letsEncryptData['stdout']); + } + + $config = \implode(PHP_EOL, [ + "tls:", + " certificates:", + " - certFile: /storage/certificates/{$domain}/fullchain.pem", + " keyFile: /storage/certificates/{$domain}/privkey.pem" + ]); + + // Save configuration into Traefik using our new cert files + if (!\file_put_contents(APP_STORAGE_CONFIG . '/' . $domain . '.yml', $config)) { + throw new Exception('Failed to save Traefik configuration.'); + } + } + + /** + * Method to make sure information about error is delivered to admnistrator. + * + * @param string $domain Domain that caused the error + * @param string $errorMessage Verbose error message + * @param int $attempt How many times it failed already + * + * @return void + */ + private function notifyError(string $domain, string $errorMessage, int $attempt): void { + // Log error into console + Console::warning('Cannot renew domain (' . $domain . ') on attempt no. ' . $attempt . ' certificate: ' . $errorMessage); + + // Send mail to administratore mail + Resque::enqueue(Event::MAILS_QUEUE_NAME, Event::MAILS_CLASS_NAME, [ + 'from' => 'console', + 'project' => 'console', + 'name' => 'Appwrite Administrator', + 'recipient' => App::getEnv('_APP_SYSTEM_SECURITY_EMAIL_ADDRESS'), + 'url' => 'https://' . $domain, + 'locale' => App::getEnv('_APP_LOCALE', 'en'), + 'type' => MAIL_TYPE_CERTIFICATE, + + 'domain' => $domain, + 'error' => $errorMessage, + 'attempt' => $attempt + ]); + } + + /** + * Update all existing domain documents so they have relation to correct certificate document. + * This solved issues: + * - when adding a domain for which there is already a certificate + * - when renew creates new document? It might? + * - overall makes it more reliable + * + * @param string $certificateId ID of a new or updated certificate document + * @param string $domain Domain that is affected by new certificate + * + * @return void + */ + private function updateDomainDocuments(string $certificateId, string $domain): void { + $domains = $this->dbForConsole->find('domains', [ + new Query('domain', Query::TYPE_EQUAL, [$domain]) + ], 1000); + + foreach ($domains as $domainDocument) { + $domainDocument->setAttribute('updated', \time()); + $domainDocument->setAttribute('certificateId', $certificateId); + + $this->dbForConsole->updateDocument('domains', $domainDocument->getId(), $domainDocument); + + if($domainDocument->getAttribute('projectId')) { + $this->dbForConsole->deleteCachedDocument('projects', $domainDocument->getAttribute('projectId')); + } + } + } } diff --git a/app/workers/deletes.php b/app/workers/deletes.php index 892d417d9c..29b9bc5c49 100644 --- a/app/workers/deletes.php +++ b/app/workers/deletes.php @@ -41,6 +41,7 @@ class DeletesV1 extends Worker public function run(): void { + $projectId = $this->args['projectId'] ?? ''; $type = $this->args['type'] ?? ''; @@ -208,13 +209,14 @@ class DeletesV1 extends Worker */ $userId = $document->getId(); - $user = $this->getProjectDB($projectId)->getDocument('users', $userId); // Delete all sessions of this user from the sessions table and update the sessions field of the user record $this->deleteByGroup('sessions', [ new Query('userId', Query::TYPE_EQUAL, [$userId]) ], $this->getProjectDB($projectId)); - + + $this->getProjectDB($projectId)->deleteCachedDocument('users', $userId); + // Delete Memberships and decrement team membership counts $this->deleteByGroup('memberships', [ new Query('userId', Query::TYPE_EQUAL, [$userId]) @@ -529,11 +531,40 @@ class DeletesV1 extends Worker */ protected function deleteCertificates(Document $document): void { + $consoleDB = $this->getConsoleDB(); + + // If domain has certificate generated + if(isset($document['certificateId'])) { + $domainUsingCertificate = $consoleDB->findOne('domains', [ + new Query('certificateId', Query::TYPE_EQUAL, [$document['certificateId']]) + ]); + + if(!$domainUsingCertificate) { + $mainDomain = App::getEnv('_APP_DOMAIN_TARGET', ''); + if($mainDomain === $document->getAttribute('domain')) { + $domainUsingCertificate = $mainDomain; + } + } + + // If certificate is still used by some domain, mark we can't delete. + // Current domain should not be found, because we only have copy. Original domain is already deleted from database. + if($domainUsingCertificate) { + Console::warning("Skipping certificate deletion, because a domain is still using it."); + return; + } + } + $domain = $document->getAttribute('domain'); $directory = APP_STORAGE_CERTIFICATES . '/' . $domain; $checkTraversal = realpath($directory) === $directory; if ($domain && $checkTraversal && is_dir($directory)) { + // Delete certificate document, so Appwrite is aware of change + if(isset($document['certificateId'])) { + $consoleDB->deleteDocument('certificates', $document['certificateId']); + } + + // Delete files, so Traefik is aware of change array_map('unlink', glob($directory . '/*.*')); rmdir($directory); Console::info("Deleted certificate files for {$domain}"); diff --git a/app/workers/mails.php b/app/workers/mails.php index 25ddb438c0..6905593a8c 100644 --- a/app/workers/mails.php +++ b/app/workers/mails.php @@ -46,6 +46,16 @@ class MailsV1 extends Worker $body = Template::fromFile(__DIR__ . '/../config/locale/templates/email-base.tpl'); $subject = ''; switch ($type) { + case MAIL_TYPE_CERTIFICATE: + $domain = $this->args['domain']; + $error = $this->args['error']; + $attempt = $this->args['attempt']; + + $subject = \sprintf($locale->getText("$prefix.subject"), $domain); + $body->setParam('{{domain}}', $domain); + $body->setParam('{{error}}', $error); + $body->setParam('{{attempt}}', $attempt); + break; case MAIL_TYPE_INVITATION: $subject = \sprintf($locale->getText("$prefix.subject"), $this->args['team'], $project); $body->setParam('{{owner}}', $this->args['owner']); @@ -126,6 +136,8 @@ class MailsV1 extends Worker switch ($type) { case MAIL_TYPE_RECOVERY: return 'emails.recovery'; + case MAIL_TYPE_CERTIFICATE: + return 'emails.certificate'; case MAIL_TYPE_INVITATION: return 'emails.invitation'; case MAIL_TYPE_VERIFICATION: diff --git a/composer.lock b/composer.lock index ea07e51fea..c2b51c52d9 100644 --- a/composer.lock +++ b/composer.lock @@ -2299,25 +2299,25 @@ }, { "name": "utopia-php/image", - "version": "0.5.3", + "version": "0.5.4", "source": { "type": "git", "url": "https://github.com/utopia-php/image.git", - "reference": "4a8429b62dcf56562b038d6712375f75166f0c02" + "reference": "ca5f436f9aa22dedaa6648f24f3687733808e336" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/image/zipball/4a8429b62dcf56562b038d6712375f75166f0c02", - "reference": "4a8429b62dcf56562b038d6712375f75166f0c02", + "url": "https://api.github.com/repos/utopia-php/image/zipball/ca5f436f9aa22dedaa6648f24f3687733808e336", + "reference": "ca5f436f9aa22dedaa6648f24f3687733808e336", "shasum": "" }, "require": { "ext-imagick": "*", - "php": ">=7.4" + "php": ">=8.0" }, "require-dev": { "phpunit/phpunit": "^9.3", - "vimeo/psalm": "4.0.1" + "vimeo/psalm": "4.13.1" }, "type": "library", "autoload": { @@ -2345,9 +2345,9 @@ ], "support": { "issues": "https://github.com/utopia-php/image/issues", - "source": "https://github.com/utopia-php/image/tree/0.5.3" + "source": "https://github.com/utopia-php/image/tree/0.5.4" }, - "time": "2021-11-02T05:47:16+00:00" + "time": "2022-05-11T12:30:41+00:00" }, { "name": "utopia-php/locale", @@ -3551,16 +3551,16 @@ }, { "name": "matthiasmullie/minify", - "version": "1.3.67", + "version": "1.3.68", "source": { "type": "git", "url": "https://github.com/matthiasmullie/minify.git", - "reference": "acaee1b7ca3cd67a39d7f98673cacd7e4739a8d9" + "reference": "c00fb02f71b2ef0a5f53fe18c5a8b9aa30f48297" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/acaee1b7ca3cd67a39d7f98673cacd7e4739a8d9", - "reference": "acaee1b7ca3cd67a39d7f98673cacd7e4739a8d9", + "url": "https://api.github.com/repos/matthiasmullie/minify/zipball/c00fb02f71b2ef0a5f53fe18c5a8b9aa30f48297", + "reference": "c00fb02f71b2ef0a5f53fe18c5a8b9aa30f48297", "shasum": "" }, "require": { @@ -3609,7 +3609,7 @@ ], "support": { "issues": "https://github.com/matthiasmullie/minify/issues", - "source": "https://github.com/matthiasmullie/minify/tree/1.3.67" + "source": "https://github.com/matthiasmullie/minify/tree/1.3.68" }, "funding": [ { @@ -3617,7 +3617,7 @@ "type": "github" } ], - "time": "2022-03-24T08:54:59+00:00" + "time": "2022-04-19T08:28:56+00:00" }, { "name": "matthiasmullie/path-converter", @@ -5711,16 +5711,16 @@ }, { "name": "symfony/console", - "version": "v6.0.7", + "version": "v6.0.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "70dcf7b2ca2ea08ad6ebcc475f104a024fb5632e" + "reference": "0d00aa289215353aa8746a31d101f8e60826285c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/70dcf7b2ca2ea08ad6ebcc475f104a024fb5632e", - "reference": "70dcf7b2ca2ea08ad6ebcc475f104a024fb5632e", + "url": "https://api.github.com/repos/symfony/console/zipball/0d00aa289215353aa8746a31d101f8e60826285c", + "reference": "0d00aa289215353aa8746a31d101f8e60826285c", "shasum": "" }, "require": { @@ -5786,7 +5786,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.0.7" + "source": "https://github.com/symfony/console/tree/v6.0.8" }, "funding": [ { @@ -5802,7 +5802,7 @@ "type": "tidelift" } ], - "time": "2022-03-31T17:18:25+00:00" + "time": "2022-04-20T15:01:42+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -6136,16 +6136,16 @@ }, { "name": "symfony/string", - "version": "v6.0.3", + "version": "v6.0.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "522144f0c4c004c80d56fa47e40e17028e2eefc2" + "reference": "ac0aa5c2282e0de624c175b68d13f2c8f2e2649d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/522144f0c4c004c80d56fa47e40e17028e2eefc2", - "reference": "522144f0c4c004c80d56fa47e40e17028e2eefc2", + "url": "https://api.github.com/repos/symfony/string/zipball/ac0aa5c2282e0de624c175b68d13f2c8f2e2649d", + "reference": "ac0aa5c2282e0de624c175b68d13f2c8f2e2649d", "shasum": "" }, "require": { @@ -6201,7 +6201,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.0.3" + "source": "https://github.com/symfony/string/tree/v6.0.8" }, "funding": [ { @@ -6217,7 +6217,7 @@ "type": "tidelift" } ], - "time": "2022-01-02T09:55:41+00:00" + "time": "2022-04-22T08:18:02+00:00" }, { "name": "textalk/websocket", diff --git a/docker-compose.yml b/docker-compose.yml index 0667f5afc5..1a00132914 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -389,6 +389,7 @@ services: environment: - _APP_ENV - _APP_OPENSSL_KEY_V1 + - _APP_DOMAIN - _APP_DOMAIN_TARGET - _APP_SYSTEM_SECURITY_EMAIL_ADDRESS - _APP_REDIS_HOST @@ -543,6 +544,11 @@ services: - _APP_REDIS_PORT - _APP_REDIS_USER - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS - _APP_MAINTENANCE_INTERVAL - _APP_MAINTENANCE_RETENTION_EXECUTION - _APP_MAINTENANCE_RETENTION_ABUSE diff --git a/public/images/users/vk.png b/public/images/users/vk.png deleted file mode 100644 index e3cf2d74ef..0000000000 Binary files a/public/images/users/vk.png and /dev/null differ diff --git a/src/Appwrite/Auth/OAuth2.php b/src/Appwrite/Auth/OAuth2.php index 4eafba7610..99b40019ac 100644 --- a/src/Appwrite/Auth/OAuth2.php +++ b/src/Appwrite/Auth/OAuth2.php @@ -7,27 +7,27 @@ abstract class OAuth2 /** * @var string */ - protected $appID; + protected string $appID; /** * @var string */ - protected $appSecret; + protected string $appSecret; /** * @var string */ - protected $callback; + protected string $callback; /** * @var array */ - protected $state; + protected array $state; /** * @var array */ - protected $scopes; + protected array $scopes; /** * OAuth2 constructor. @@ -52,66 +52,69 @@ abstract class OAuth2 /** * @return string */ - abstract public function getName():string; + abstract public function getName(): string; /** * @return string */ - abstract public function getLoginURL():string; + abstract public function getLoginURL(): string; /** * @param string $code * * @return array */ - abstract protected function getTokens(string $code):array; + abstract protected function getTokens(string $code): array; /** * @param string $refreshToken * * @return array */ - abstract public function refreshTokens(string $refreshToken):array; + abstract public function refreshTokens(string $refreshToken): array; /** - * @param $accessToken + * @param string $accessToken * * @return string */ - abstract public function getUserID(string $accessToken):string; + abstract public function getUserEmail(string $accessToken): string; /** - * @param $accessToken + * Check if the OAuth email is verified + * + * @param string $accessToken + * + * @return bool + */ + abstract public function isEmailVerified(string $accessToken): bool; + + /** + * @param string $accessToken * * @return string */ - abstract public function getUserEmail(string $accessToken):string; - - /** - * @param $accessToken - * - * @return string - */ - abstract public function getUserName(string $accessToken):string; + abstract public function getUserName(string $accessToken): string; /** * @param $scope * * @return $this */ - protected function addScope(string $scope):OAuth2 + protected function addScope(string $scope): OAuth2 { // Add a scope to the scopes array if it isn't already present if (!\in_array($scope, $this->scopes)) { $this->scopes[] = $scope; } + return $this; } /** * @return array */ - protected function getScopes():array + protected function getScopes(): array { return $this->scopes; } @@ -121,9 +124,10 @@ abstract class OAuth2 * * @return string */ - public function getAccessToken(string $code):string + public function getAccessToken(string $code): string { $tokens = $this->getTokens($code); + return $tokens['access_token'] ?? ''; } @@ -132,9 +136,10 @@ abstract class OAuth2 * * @return string */ - public function getRefreshToken(string $code):string + public function getRefreshToken(string $code): string { $tokens = $this->getTokens($code); + return $tokens['refresh_token'] ?? ''; } @@ -143,9 +148,10 @@ abstract class OAuth2 * * @return string */ - public function getAccessTokenExpiry(string $code):string + public function getAccessTokenExpiry(string $code): string { $tokens = $this->getTokens($code); + return $tokens['expires_in'] ?? ''; } @@ -170,7 +176,7 @@ abstract class OAuth2 * * @return string */ - protected function request(string $method, string $url = '', array $headers = [], string $payload = ''):string + protected function request(string $method, string $url = '', array $headers = [], string $payload = ''): string { $ch = \curl_init($url); diff --git a/src/Appwrite/Auth/OAuth2/Amazon.php b/src/Appwrite/Auth/OAuth2/Amazon.php index 7ec2769c9a..2e72975fd1 100644 --- a/src/Appwrite/Auth/OAuth2/Amazon.php +++ b/src/Appwrite/Auth/OAuth2/Amazon.php @@ -14,17 +14,17 @@ class Amazon extends OAuth2 /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = [ + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = [ "profile" ]; @@ -37,7 +37,7 @@ class Amazon extends OAuth2 } /** - * @param $state + * @param string $state * * @return array */ @@ -52,13 +52,13 @@ class Amazon extends OAuth2 */ public function getLoginURL(): string { - return 'https://www.amazon.com/ap/oa?'.\http_build_query([ - 'response_type' => 'code', - 'client_id' => $this->appID, - 'scope' => \implode(' ', $this->getScopes()), - 'state' => \json_encode($this->state), - 'redirect_uri' => $this->callback - ]); + return 'https://www.amazon.com/ap/oa?' . \http_build_query([ + 'response_type' => 'code', + 'client_id' => $this->appID, + 'scope' => \implode(' ', $this->getScopes()), + 'state' => \json_encode($this->state), + 'redirect_uri' => $this->callback + ]); } /** @@ -68,7 +68,7 @@ class Amazon extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = ['Content-Type: application/x-www-form-urlencoded;charset=UTF-8']; $this->tokens = \json_decode($this->request( 'POST', @@ -92,7 +92,7 @@ class Amazon extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $headers = ['Content-Type: application/x-www-form-urlencoded;charset=UTF-8']; $this->tokens = \json_decode($this->request( @@ -107,7 +107,7 @@ class Amazon extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -123,11 +123,7 @@ class Amazon extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['user_id'])) { - return $user['user_id']; - } - - return ''; + return $user['user_id'] ?? ''; } /** @@ -139,11 +135,23 @@ class Amazon extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['email'])) { - return $user['email']; - } + return $user['email'] ?? ''; + } - return ''; + /** + * Check if the OAuth email is verified + * + * If present, the email is verified. This was verfied through a manual Amazon sign up process + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $email = $this->getUserEmail($accessToken); + + return !empty($email); } /** @@ -155,11 +163,7 @@ class Amazon extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['name'])) { - return $user['name']; - } - - return ''; + return $user['name'] ?? ''; } /** @@ -170,7 +174,7 @@ class Amazon extends OAuth2 protected function getUser(string $accessToken): array { if (empty($this->user)) { - $user = $this->request('GET', 'https://api.amazon.com/user/profile?access_token='.\urlencode($accessToken)); + $user = $this->request('GET', 'https://api.amazon.com/user/profile?access_token=' . \urlencode($accessToken)); $this->user = \json_decode($user, true); } return $this->user; diff --git a/src/Appwrite/Auth/OAuth2/Apple.php b/src/Appwrite/Auth/OAuth2/Apple.php index f1ff4b724d..1ea352e685 100644 --- a/src/Appwrite/Auth/OAuth2/Apple.php +++ b/src/Appwrite/Auth/OAuth2/Apple.php @@ -13,17 +13,17 @@ class Apple extends OAuth2 /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = [ + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = [ "name", "email" ]; @@ -31,7 +31,7 @@ class Apple extends OAuth2 /** * @var array */ - protected $claims = []; + protected array $claims = []; /** * @return string @@ -40,13 +40,13 @@ class Apple extends OAuth2 { return 'apple'; } - + /** * @return string */ public function getLoginURL(): string { - return 'https://appleid.apple.com/auth/authorize?'.\http_build_query([ + return 'https://appleid.apple.com/auth/authorize?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, 'state' => \json_encode($this->state), @@ -63,7 +63,7 @@ class Apple extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', @@ -90,7 +90,7 @@ class Apple extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( @@ -105,7 +105,7 @@ class Apple extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -122,11 +122,7 @@ class Apple extends OAuth2 */ public function getUserID(string $accessToken): string { - if (isset($this->claims['sub']) && !empty($this->claims['sub'])) { - return $this->claims['sub']; - } - - return ''; + return $this->claims['sub'] ?? ''; } /** @@ -136,14 +132,25 @@ class Apple extends OAuth2 */ public function getUserEmail(string $accessToken): string { - if (isset($this->claims['email']) && - !empty($this->claims['email']) && - isset($this->claims['email_verified']) && - $this->claims['email_verified'] === 'true') { - return $this->claims['email']; + return $this->claims['email'] ?? ''; + } + + /** + * Check if the OAuth email is verified + * + * @link https://developer.apple.com/forums/thread/121411 + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + if ($this->claims['email_verified'] ?? false) { + return true; } - return ''; + return false; } /** @@ -153,17 +160,19 @@ class Apple extends OAuth2 */ public function getUserName(string $accessToken): string { - if (isset($this->claims['email']) && + if ( + isset($this->claims['email']) && !empty($this->claims['email']) && isset($this->claims['email_verified']) && - $this->claims['email_verified'] === 'true') { + $this->claims['email_verified'] === 'true' + ) { return $this->claims['email']; } return ''; } - protected function getAppSecret():string + protected function getAppSecret(): string { try { $secret = \json_decode($this->appSecret, true); @@ -180,18 +189,18 @@ class Apple extends OAuth2 'alg' => 'ES256', 'kid' => $keyID, ]; - + $claims = [ 'iss' => $teamID, 'iat' => \time(), - 'exp' => \time() + 86400*180, + 'exp' => \time() + 86400 * 180, 'aud' => 'https://appleid.apple.com', 'sub' => $bundleID, ]; $pkey = \openssl_pkey_get_private($keyfile); - $payload = $this->encode(\json_encode($headers)).'.'.$this->encode(\json_encode($claims)); + $payload = $this->encode(\json_encode($headers)) . '.' . $this->encode(\json_encode($claims)); $signature = ''; @@ -201,7 +210,7 @@ class Apple extends OAuth2 return ''; } - return $payload.'.'.$this->encode($this->fromDER($signature, 64)); + return $payload . '.' . $this->encode($this->fromDER($signature, 64)); } /** @@ -230,10 +239,10 @@ class Apple extends OAuth2 * @param string $der * @param int $partLength */ - protected function fromDER(string $der, int $partLength):string + protected function fromDER(string $der, int $partLength): string { $hex = \unpack('H*', $der)[1]; - + if ('30' !== \mb_substr($hex, 0, 2, '8bit')) { // SEQUENCE throw new \RuntimeException(); } @@ -252,7 +261,7 @@ class Apple extends OAuth2 $R = \str_pad($R, $partLength, '0', STR_PAD_LEFT); $hex = \mb_substr($hex, 4 + $Rl * 2, null, '8bit'); - + if ('02' !== \mb_substr($hex, 0, 2, '8bit')) { // INTEGER throw new \RuntimeException(); } @@ -261,6 +270,6 @@ class Apple extends OAuth2 $S = $this->retrievePositiveInteger(\mb_substr($hex, 4, $Sl * 2, '8bit')); $S = \str_pad($S, $partLength, '0', STR_PAD_LEFT); - return \pack('H*', $R.$S); + return \pack('H*', $R . $S); } } diff --git a/src/Appwrite/Auth/OAuth2/Auth0.php b/src/Appwrite/Auth/OAuth2/Auth0.php index b1c9c8ce1f..4775139a0b 100644 --- a/src/Appwrite/Auth/OAuth2/Auth0.php +++ b/src/Appwrite/Auth/OAuth2/Auth0.php @@ -8,27 +8,27 @@ use Appwrite\Auth\OAuth2; // https://auth0.com/docs/api/authentication class Auth0 extends OAuth2 -{ - /** +{ + /** * @var array */ - protected $scopes = [ + protected array $scopes = [ 'openid', 'profile', 'email', 'offline_access' ]; - + /** * @var array */ - protected $user = []; - + protected array $user = []; + /** * @var array */ - protected $tokens = []; - + protected array $tokens = []; + /** * @return string */ @@ -42,11 +42,11 @@ class Auth0 extends OAuth2 */ public function getLoginURL(): string { - return 'https://'.$this->getAuth0Domain().'/authorize?'.\http_build_query([ + return 'https://' . $this->getAuth0Domain() . '/authorize?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, - 'state'=> \json_encode($this->state), - 'scope'=> \implode(' ', $this->getScopes()), + 'state' => \json_encode($this->state), + 'scope' => \implode(' ', $this->getScopes()), 'response_type' => 'code' ]); } @@ -58,11 +58,11 @@ class Auth0 extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', - 'https://'.$this->getAuth0Domain().'/oauth/token', + 'https://' . $this->getAuth0Domain() . '/oauth/token', $headers, \http_build_query([ 'code' => $code, @@ -77,8 +77,8 @@ class Auth0 extends OAuth2 return $this->tokens; } - - + + /** * @param string $refreshToken * @@ -89,7 +89,7 @@ class Auth0 extends OAuth2 $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', - 'https://'.$this->getAuth0Domain().'/oauth/token', + 'https://' . $this->getAuth0Domain() . '/oauth/token', $headers, \http_build_query([ 'refresh_token' => $refreshToken, @@ -99,7 +99,7 @@ class Auth0 extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -114,12 +114,8 @@ class Auth0 extends OAuth2 public function getUserID(string $accessToken): string { $user = $this->getUser($accessToken); - - if (isset($user['sub'])) { - return $user['sub']; - } - - return ''; + + return $user['sub'] ?? ''; } /** @@ -130,12 +126,28 @@ class Auth0 extends OAuth2 public function getUserEmail(string $accessToken): string { $user = $this->getUser($accessToken); - - if (isset($user['email'])) { - return $user['email']; + + return $user['email'] ?? ''; + } + + /** + * Check if the OAuth email is verified + * + * @link https://auth0.com/docs/api/authentication?javascript#user-profile + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if ($user['email_verified'] ?? false) { + return true; } - - return ''; + + return false; } /** @@ -146,15 +158,11 @@ class Auth0 extends OAuth2 public function getUserName(string $accessToken): string { $user = $this->getUser($accessToken); - - if (isset($user['name'])) { - return $user['name']; - } - - return ''; + + return $user['name'] ?? ''; } - - /** + + /** * @param string $accessToken * * @return array @@ -162,8 +170,8 @@ class Auth0 extends OAuth2 protected function getUser(string $accessToken): array { if (empty($this->user)) { - $headers = ['Authorization: Bearer '. \urlencode($accessToken)]; - $user = $this->request('GET', 'https://'.$this->getAuth0Domain().'/userinfo', $headers); + $headers = ['Authorization: Bearer ' . \urlencode($accessToken)]; + $user = $this->request('GET', 'https://' . $this->getAuth0Domain() . '/userinfo', $headers); $this->user = \json_decode($user, true); } @@ -179,10 +187,10 @@ class Auth0 extends OAuth2 { $secret = $this->getAppSecret(); - return (isset($secret['clientSecret'])) ? $secret['clientSecret'] : ''; + return $secret['clientSecret'] ?? ''; } - /** + /** * Extracts the Auth0 Domain from the JSON stored in appSecret * * @return string @@ -190,7 +198,8 @@ class Auth0 extends OAuth2 protected function getAuth0Domain(): string { $secret = $this->getAppSecret(); - return (isset($secret['auth0Domain'])) ? $secret['auth0Domain'] : ''; + + return $secret['auth0Domain'] ?? ''; } /** @@ -199,7 +208,7 @@ class Auth0 extends OAuth2 * @return array */ protected function getAppSecret(): array - { + { try { $secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR); } catch (\Throwable $th) { @@ -207,4 +216,4 @@ class Auth0 extends OAuth2 } return $secret; } -} \ No newline at end of file +} diff --git a/src/Appwrite/Auth/OAuth2/Bitbucket.php b/src/Appwrite/Auth/OAuth2/Bitbucket.php index 74d684c845..bbefe144c1 100644 --- a/src/Appwrite/Auth/OAuth2/Bitbucket.php +++ b/src/Appwrite/Auth/OAuth2/Bitbucket.php @@ -12,17 +12,17 @@ class Bitbucket extends OAuth2 /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = []; + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = []; /** * @return string @@ -37,12 +37,12 @@ class Bitbucket extends OAuth2 */ public function getLoginURL(): string { - return 'https://bitbucket.org/site/oauth2/authorize?'.\http_build_query([ - 'response_type' => 'code', - 'client_id' => $this->appID, - 'scope' => \implode(' ', $this->getScopes()), - 'state' => \json_encode($this->state), - ]); + return 'https://bitbucket.org/site/oauth2/authorize?' . \http_build_query([ + 'response_type' => 'code', + 'client_id' => $this->appID, + 'scope' => \implode(' ', $this->getScopes()), + 'state' => \json_encode($this->state), + ]); } /** @@ -52,7 +52,7 @@ class Bitbucket extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { // Required as per Bitbucket Spec. $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( @@ -76,7 +76,7 @@ class Bitbucket extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( @@ -91,7 +91,7 @@ class Bitbucket extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -107,11 +107,7 @@ class Bitbucket extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['uuid'])) { - return $user['uuid']; - } - - return ''; + return $user['uuid'] ?? ''; } /** @@ -123,11 +119,25 @@ class Bitbucket extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['email'])) { - return $user['email']; + return $user['email'] ?? ''; + } + + /** + * Check if the OAuth email is verified + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if ($user['is_confirmed'] ?? false) { + return true; } - return ''; + return false; } /** @@ -139,11 +149,7 @@ class Bitbucket extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['display_name'])) { - return $user['display_name']; - } - - return ''; + return $user['display_name'] ?? ''; } /** @@ -154,11 +160,20 @@ class Bitbucket extends OAuth2 protected function getUser(string $accessToken): array { if (empty($this->user)) { - $user = $this->request('GET', 'https://api.bitbucket.org/2.0/user?access_token='.\urlencode($accessToken)); + $user = $this->request('GET', 'https://api.bitbucket.org/2.0/user?access_token=' . \urlencode($accessToken)); $this->user = \json_decode($user, true); - $email = $this->request('GET', 'https://api.bitbucket.org/2.0/user/emails?access_token='.\urlencode($accessToken)); - $this->user['email'] = \json_decode($email, true)['values'][0]['email']; + $emails = $this->request('GET', 'https://api.bitbucket.org/2.0/user/emails?access_token=' . \urlencode($accessToken)); + $emails = \json_decode($emails, true); + if (isset($emails['values'])) { + foreach ($emails['values'] as $email) { + if ($email['is_confirmed']) { + $this->user['email'] = $email['email']; + $this->user['is_confirmed'] = $email['is_confirmed']; + break; + } + } + } } return $this->user; } diff --git a/src/Appwrite/Auth/OAuth2/Bitly.php b/src/Appwrite/Auth/OAuth2/Bitly.php index 02a21dc29e..08350bea6f 100644 --- a/src/Appwrite/Auth/OAuth2/Bitly.php +++ b/src/Appwrite/Auth/OAuth2/Bitly.php @@ -3,7 +3,6 @@ namespace Appwrite\Auth\OAuth2; use Appwrite\Auth\OAuth2; -use Utopia\Exception; // Reference Material // https://dev.bitly.com/v4_documentation.html @@ -14,32 +13,32 @@ class Bitly extends OAuth2 /** * @var string */ - private $endpoint = 'https://bitly.com/oauth/'; + private string $endpoint = 'https://bitly.com/oauth/'; /** * @var string */ - private $resourceEndpoint = 'https://api-ssl.bitly.com/'; + private string $resourceEndpoint = 'https://api-ssl.bitly.com/'; /** * @var array */ - protected $scopes = []; + protected array $scopes = []; /** * @var array */ - protected $user = []; - + protected array $user = []; + /** * @var array */ - protected $tokens = []; + protected array $tokens = []; /** * @return string */ - public function getName():string + public function getName(): string { return 'bitly'; } @@ -47,9 +46,9 @@ class Bitly extends OAuth2 /** * @return string */ - public function getLoginURL():string + public function getLoginURL(): string { - return $this->endpoint . 'authorize?'. + return $this->endpoint . 'authorize?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, @@ -64,7 +63,7 @@ class Bitly extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $response = $this->request( 'POST', $this->resourceEndpoint . 'oauth/access_token', @@ -91,7 +90,7 @@ class Bitly extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $response = $this->request( 'POST', @@ -109,7 +108,7 @@ class Bitly extends OAuth2 \parse_str($response, $output); $this->tokens = $output; - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -117,51 +116,61 @@ class Bitly extends OAuth2 } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserID(string $accessToken):string + public function getUserID(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['login'])) { - return $user['login']; - } - - return ''; + return $user['login'] ?? ''; } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserEmail(string $accessToken):string + public function getUserEmail(string $accessToken): string { $user = $this->getUser($accessToken); if (isset($user['emails'])) { - return $user['emails'][0]['email']; + foreach ($user['emails'] as $email) { + if ($email['is_verified'] === true) { + return $email['email']; + } + } } return ''; } /** - * @param $accessToken + * Check if the OAuth email is verified + * + * @link https://dev.bitly.com/api-reference#getUser + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + return true; + } + + /** + * @param string $accessToken * * @return string */ - public function getUserName(string $accessToken):string + public function getUserName(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['name'])) { - return $user['name']; - } - - return ''; + return $user['name'] ?? ''; } /** @@ -172,7 +181,7 @@ class Bitly extends OAuth2 protected function getUser(string $accessToken) { $headers = [ - 'Authorization: Bearer '. \urlencode($accessToken), + 'Authorization: Bearer ' . \urlencode($accessToken), "Accept: application/json" ]; @@ -180,7 +189,6 @@ class Bitly extends OAuth2 $this->user = \json_decode($this->request('GET', $this->resourceEndpoint . "v4/user", $headers), true); } - return $this->user; } } diff --git a/src/Appwrite/Auth/OAuth2/Box.php b/src/Appwrite/Auth/OAuth2/Box.php index 80d59e9198..da925eed1a 100644 --- a/src/Appwrite/Auth/OAuth2/Box.php +++ b/src/Appwrite/Auth/OAuth2/Box.php @@ -12,27 +12,27 @@ class Box extends OAuth2 /** * @var string */ - private $endpoint = 'https://account.box.com/api/oauth2/'; + private string $endpoint = 'https://account.box.com/api/oauth2/'; /** * @var string */ - private $resourceEndpoint = 'https://api.box.com/2.0/'; + private string $resourceEndpoint = 'https://api.box.com/2.0/'; /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = [ + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = [ 'manage_app_users', ]; @@ -49,7 +49,7 @@ class Box extends OAuth2 */ public function getLoginURL(): string { - $url = $this->endpoint . 'authorize?'. + $url = $this->endpoint . 'authorize?' . \http_build_query([ 'response_type' => 'code', 'client_id' => $this->appID, @@ -68,7 +68,7 @@ class Box extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', @@ -93,7 +93,7 @@ class Box extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( @@ -108,7 +108,7 @@ class Box extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -124,11 +124,7 @@ class Box extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['id'])) { - return $user['id']; - } - - return ''; + return $user['id'] ?? ''; } /** @@ -140,11 +136,23 @@ class Box extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['login'])) { - return $user['login']; - } + return $user['login'] ?? ''; + } - return ''; + /** + * Check if the OAuth email is verified + * + * If present, the email is verified. This was verfied through a manual Box sign up process + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $email = $this->getUserEmail($accessToken); + + return !empty($email); } /** @@ -156,11 +164,7 @@ class Box extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['name'])) { - return $user['name']; - } - - return ''; + return $user['name'] ?? ''; } /** @@ -171,7 +175,7 @@ class Box extends OAuth2 protected function getUser(string $accessToken): array { $header = [ - 'Authorization: Bearer '.\urlencode($accessToken), + 'Authorization: Bearer ' . \urlencode($accessToken), ]; if (empty($this->user)) { $user = $this->request( diff --git a/src/Appwrite/Auth/OAuth2/Discord.php b/src/Appwrite/Auth/OAuth2/Discord.php index dece646e55..7cf2ef1b7b 100644 --- a/src/Appwrite/Auth/OAuth2/Discord.php +++ b/src/Appwrite/Auth/OAuth2/Discord.php @@ -12,22 +12,22 @@ class Discord extends OAuth2 /** * @var string */ - private $endpoint = 'https://discordapp.com/api'; + private string $endpoint = 'https://discordapp.com/api'; /** * @var array */ - protected $user = []; + protected array $user = []; /** * @var array */ - protected $tokens = []; + protected array $tokens = []; /** * @var array */ - protected $scopes = [ + protected array $scopes = [ 'identify', 'email' ]; @@ -118,11 +118,7 @@ class Discord extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['id'])) { - return $user['id']; - } - - return ''; + return $user['id'] ?? ''; } /** @@ -134,11 +130,27 @@ class Discord extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['email'])) { - return $user['email']; + return $user['email'] ?? ''; + } + + /** + * Check if the OAuth email is verified + * + * @link https://discord.com/developers/docs/resources/user + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if ($user['verified'] ?? false) { + return true; } - return ''; + return false; } /** @@ -150,11 +162,7 @@ class Discord extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['username'])) { - return $user['username']; - } - - return ''; + return $user['username'] ?? ''; } /** diff --git a/src/Appwrite/Auth/OAuth2/Dropbox.php b/src/Appwrite/Auth/OAuth2/Dropbox.php index f67e10b01d..5025cb3dc8 100644 --- a/src/Appwrite/Auth/OAuth2/Dropbox.php +++ b/src/Appwrite/Auth/OAuth2/Dropbox.php @@ -13,17 +13,17 @@ class Dropbox extends OAuth2 /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = []; + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = []; /** * @return string @@ -32,17 +32,17 @@ class Dropbox extends OAuth2 { return 'dropbox'; } - + /** * @return string */ public function getLoginURL(): string { - return 'https://www.dropbox.com/oauth2/authorize?'.\http_build_query([ - 'client_id' => $this->appID, - 'redirect_uri' => $this->callback, - 'state' => \json_encode($this->state), - 'response_type' => 'code' + return 'https://www.dropbox.com/oauth2/authorize?' . \http_build_query([ + 'client_id' => $this->appID, + 'redirect_uri' => $this->callback, + 'state' => \json_encode($this->state), + 'response_type' => 'code' ]); } @@ -53,7 +53,7 @@ class Dropbox extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', @@ -77,7 +77,7 @@ class Dropbox extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( @@ -92,7 +92,7 @@ class Dropbox extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -108,11 +108,7 @@ class Dropbox extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['account_id'])) { - return $user['account_id']; - } - - return ''; + return $user['account_id'] ?? ''; } /** @@ -124,11 +120,27 @@ class Dropbox extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['email'])) { - return $user['email']; + return $user['email'] ?? ''; + } + + /** + * Check if the OAuth email is verified + * + * @link https://www.dropbox.com/developers/documentation/http/documentation#users-get_current_account + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if ($user['email_verified'] ?? false) { + return true; } - return ''; + return false; } /** @@ -140,11 +152,7 @@ class Dropbox extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['name'])) { - return $user['name']['display_name']; - } - - return ''; + return $user['name']['display_name'] ?? ''; } /** @@ -155,7 +163,7 @@ class Dropbox extends OAuth2 protected function getUser(string $accessToken): array { if (empty($this->user)) { - $headers = ['Authorization: Bearer '. \urlencode($accessToken)]; + $headers = ['Authorization: Bearer ' . \urlencode($accessToken)]; $user = $this->request('POST', 'https://api.dropboxapi.com/2/users/get_current_account', $headers); $this->user = \json_decode($user, true); } diff --git a/src/Appwrite/Auth/OAuth2/Facebook.php b/src/Appwrite/Auth/OAuth2/Facebook.php index 7759a54042..555daedc58 100644 --- a/src/Appwrite/Auth/OAuth2/Facebook.php +++ b/src/Appwrite/Auth/OAuth2/Facebook.php @@ -3,36 +3,35 @@ namespace Appwrite\Auth\OAuth2; use Appwrite\Auth\OAuth2; -use Utopia\Exception; class Facebook extends OAuth2 { /** * @var string */ - protected $version = 'v2.8'; + protected string $version = 'v2.8'; /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = [ + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = [ 'email' ]; /** * @return string */ - public function getName():string + public function getName(): string { return 'facebook'; } @@ -40,10 +39,10 @@ class Facebook extends OAuth2 /** * @return string */ - public function getLoginURL():string + public function getLoginURL(): string { - return 'https://www.facebook.com/'.$this->version.'/dialog/oauth?'.\http_build_query([ - 'client_id'=> $this->appID, + return 'https://www.facebook.com/' . $this->version . '/dialog/oauth?' . \http_build_query([ + 'client_id' => $this->appID, 'redirect_uri' => $this->callback, 'scope' => \implode(' ', $this->getScopes()), 'state' => \json_encode($this->state) @@ -57,7 +56,7 @@ class Facebook extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $this->tokens = \json_decode($this->request( 'GET', 'https://graph.facebook.com/' . $this->version . '/oauth/access_token?' . \http_build_query([ @@ -77,7 +76,7 @@ class Facebook extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $this->tokens = \json_decode($this->request( 'GET', @@ -90,7 +89,7 @@ class Facebook extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -102,15 +101,11 @@ class Facebook extends OAuth2 * * @return string */ - public function getUserID(string $accessToken):string + public function getUserID(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['id'])) { - return $user['id']; - } - - return ''; + return $user['id'] ?? ''; } /** @@ -118,15 +113,27 @@ class Facebook extends OAuth2 * * @return string */ - public function getUserEmail(string $accessToken):string + public function getUserEmail(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['email'])) { - return $user['email']; - } + return $user['email'] ?? ''; + } - return ''; + /** + * Check if the OAuth email is verified + * + * If present, the email is verified. This was verfied through a manual Facebook sign up process + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $email = $this->getUserEmail($accessToken); + + return !empty($email); } /** @@ -134,15 +141,11 @@ class Facebook extends OAuth2 * * @return string */ - public function getUserName(string $accessToken):string + public function getUserName(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['name'])) { - return $user['name']; - } - - return ''; + return $user['name'] ?? ''; } /** @@ -150,10 +153,10 @@ class Facebook extends OAuth2 * * @return array */ - protected function getUser(string $accessToken):array + protected function getUser(string $accessToken): array { if (empty($this->user)) { - $user = $this->request('GET', 'https://graph.facebook.com/'.$this->version.'/me?fields=email,name&access_token='.\urlencode($accessToken)); + $user = $this->request('GET', 'https://graph.facebook.com/' . $this->version . '/me?fields=email,name&access_token=' . \urlencode($accessToken)); $this->user = \json_decode($user, true); } diff --git a/src/Appwrite/Auth/OAuth2/Github.php b/src/Appwrite/Auth/OAuth2/Github.php index dddd4a5181..1130134f68 100644 --- a/src/Appwrite/Auth/OAuth2/Github.php +++ b/src/Appwrite/Auth/OAuth2/Github.php @@ -3,31 +3,30 @@ namespace Appwrite\Auth\OAuth2; use Appwrite\Auth\OAuth2; -use Utopia\Exception; class Github extends OAuth2 { /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = [ + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = [ 'user:email', ]; /** * @return string */ - public function getName():string + public function getName(): string { return 'github'; } @@ -35,9 +34,9 @@ class Github extends OAuth2 /** * @return string */ - public function getLoginURL():string + public function getLoginURL(): string { - return 'https://github.com/login/oauth/authorize?'. \http_build_query([ + return 'https://github.com/login/oauth/authorize?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, 'scope' => \implode(' ', $this->getScopes()), @@ -52,7 +51,7 @@ class Github extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $response = $this->request( 'POST', 'https://github.com/login/oauth/access_token', @@ -78,7 +77,7 @@ class Github extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $response = $this->request( 'POST', @@ -96,7 +95,7 @@ class Github extends OAuth2 \parse_str($response, $output); $this->tokens = $output; - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -104,53 +103,59 @@ class Github extends OAuth2 } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserID(string $accessToken):string + public function getUserID(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['id'])) { - return $user['id']; - } - - return ''; + return $user['id'] ?? ''; } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserEmail(string $accessToken):string - { - $emails = \json_decode($this->request('GET', 'https://api.github.com/user/emails', ['Authorization: token '.\urlencode($accessToken)]), true); - - foreach ($emails as $email) { - if ($email['primary'] && $email['verified']) { - return $email['email']; - } - } - - return ''; - } - - /** - * @param $accessToken - * - * @return string - */ - public function getUserName(string $accessToken):string + public function getUserEmail(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['name'])) { - return $user['name']; + return $user['email'] ?? ''; + } + + /** + * Check if the OAuth email is verified + * + * @link https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if ($user['verified'] ?? false) { + return true; } - return ''; + return false; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserName(string $accessToken): string + { + $user = $this->getUser($accessToken); + + return $user['name'] ?? ''; } /** @@ -161,7 +166,18 @@ class Github extends OAuth2 protected function getUser(string $accessToken) { if (empty($this->user)) { - $this->user = \json_decode($this->request('GET', 'https://api.github.com/user', ['Authorization: token '.\urlencode($accessToken)]), true); + $this->user = \json_decode($this->request('GET', 'https://api.github.com/user', ['Authorization: token ' . \urlencode($accessToken)]), true); + + $emails = $this->request('GET', 'https://api.github.com/user/emails', ['Authorization: token ' . \urlencode($accessToken)]); + + $emails = \json_decode($emails, true); + foreach ($emails as $email) { + if (isset($email['verified']) && $email['verified'] === true) { + $this->user['email'] = $email['email']; + $this->user['verified'] = $email['verified']; + break; + } + } } return $this->user; diff --git a/src/Appwrite/Auth/OAuth2/Gitlab.php b/src/Appwrite/Auth/OAuth2/Gitlab.php index 19ee1363a7..fa0a93df92 100644 --- a/src/Appwrite/Auth/OAuth2/Gitlab.php +++ b/src/Appwrite/Auth/OAuth2/Gitlab.php @@ -12,17 +12,17 @@ class Gitlab extends OAuth2 /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = [ + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = [ 'read_user' ]; @@ -39,7 +39,7 @@ class Gitlab extends OAuth2 */ public function getLoginURL(): string { - return 'https://gitlab.com/oauth/authorize?'.\http_build_query([ + return 'https://gitlab.com/oauth/authorize?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, 'scope' => \implode(' ', $this->getScopes()), @@ -55,7 +55,7 @@ class Gitlab extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $this->tokens = \json_decode($this->request( 'POST', 'https://gitlab.com/oauth/token?' . \http_build_query([ @@ -76,7 +76,7 @@ class Gitlab extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $this->tokens = \json_decode($this->request( 'POST', @@ -88,7 +88,7 @@ class Gitlab extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -120,11 +120,27 @@ class Gitlab extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['email'])) { - return $user['email']; + return $user['email'] ?? ''; + } + + /** + * Check if the OAuth email is verified + * + * @link https://docs.gitlab.com/ee/api/users.html#list-current-user-for-normal-users + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if ($user['confirmed_at'] ?? false) { + return true; } - return ''; + return false; } /** @@ -136,11 +152,7 @@ class Gitlab extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['name'])) { - return $user['name']; - } - - return ''; + return $user['name'] ?? ''; } /** @@ -151,7 +163,7 @@ class Gitlab extends OAuth2 protected function getUser(string $accessToken): array { if (empty($this->user)) { - $user = $this->request('GET', 'https://gitlab.com/api/v4/user?access_token='.\urlencode($accessToken)); + $user = $this->request('GET', 'https://gitlab.com/api/v4/user?access_token=' . \urlencode($accessToken)); $this->user = \json_decode($user, true); } diff --git a/src/Appwrite/Auth/OAuth2/Google.php b/src/Appwrite/Auth/OAuth2/Google.php index 5b3ab938b3..e675a1f861 100644 --- a/src/Appwrite/Auth/OAuth2/Google.php +++ b/src/Appwrite/Auth/OAuth2/Google.php @@ -14,12 +14,12 @@ class Google extends OAuth2 /** * @var string */ - protected $version = 'v4'; + protected string $version = 'v4'; /** * @var array */ - protected $scopes = [ + protected array $scopes = [ 'https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile', 'openid' @@ -28,12 +28,12 @@ class Google extends OAuth2 /** * @var array */ - protected $user = []; - + protected array $user = []; + /** * @var array */ - protected $tokens = []; + protected array $tokens = []; /** * @return string @@ -48,7 +48,7 @@ class Google extends OAuth2 */ public function getLoginURL(): string { - return 'https://accounts.google.com/o/oauth2/v2/auth?'. \http_build_query([ + return 'https://accounts.google.com/o/oauth2/v2/auth?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, 'scope' => \implode(' ', $this->getScopes()), @@ -64,7 +64,7 @@ class Google extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $this->tokens = \json_decode($this->request( 'POST', 'https://oauth2.googleapis.com/token?' . \http_build_query([ @@ -86,7 +86,7 @@ class Google extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $this->tokens = \json_decode($this->request( 'POST', @@ -98,7 +98,7 @@ class Google extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -114,11 +114,7 @@ class Google extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['id'])) { - return $user['id']; - } - - return ''; + return $user['id'] ?? ''; } /** @@ -130,11 +126,27 @@ class Google extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['email'])) { - return $user['email']; + return $user['email'] ?? ''; + } + + /** + * Check if the OAuth email is verified + * + * @link https://www.oauth.com/oauth2-servers/signing-in-with-google/verifying-the-user-info/ + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if ($user['email_verified'] ?? false) { + return true; } - return ''; + return false; } /** @@ -146,11 +158,7 @@ class Google extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['name'])) { - return $user['name']; - } - - return ''; + return $user['name'] ?? ''; } /** @@ -161,7 +169,7 @@ class Google extends OAuth2 protected function getUser(string $accessToken): array { if (empty($this->user)) { - $user = $this->request('GET', 'https://www.googleapis.com/oauth2/v2/userinfo?access_token='.\urlencode($accessToken)); + $user = $this->request('GET', 'https://www.googleapis.com/oauth2/v3/userinfo?access_token=' . \urlencode($accessToken)); $this->user = \json_decode($user, true); } diff --git a/src/Appwrite/Auth/OAuth2/Linkedin.php b/src/Appwrite/Auth/OAuth2/Linkedin.php index 2519859a1b..3ada765319 100644 --- a/src/Appwrite/Auth/OAuth2/Linkedin.php +++ b/src/Appwrite/Auth/OAuth2/Linkedin.php @@ -9,17 +9,17 @@ class Linkedin extends OAuth2 /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = [ + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = [ 'r_liteprofile', 'r_emailaddress', ]; @@ -40,7 +40,7 @@ class Linkedin extends OAuth2 /** * @return string */ - public function getName():string + public function getName(): string { return 'linkedin'; } @@ -48,15 +48,15 @@ class Linkedin extends OAuth2 /** * @return string */ - public function getLoginURL():string + public function getLoginURL(): string { - return 'https://www.linkedin.com/oauth/v2/authorization?'.\http_build_query([ - 'response_type' => 'code', - 'client_id' => $this->appID, - 'redirect_uri' => $this->callback, - 'scope' => \implode(' ', $this->getScopes()), - 'state' => \json_encode($this->state), - ]); + return 'https://www.linkedin.com/oauth/v2/authorization?' . \http_build_query([ + 'response_type' => 'code', + 'client_id' => $this->appID, + 'redirect_uri' => $this->callback, + 'scope' => \implode(' ', $this->getScopes()), + 'state' => \json_encode($this->state), + ]); } /** @@ -66,7 +66,7 @@ class Linkedin extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $this->tokens = \json_decode($this->request( 'POST', 'https://www.linkedin.com/oauth/v2/accessToken', @@ -89,7 +89,7 @@ class Linkedin extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $this->tokens = \json_decode($this->request( 'POST', @@ -104,7 +104,7 @@ class Linkedin extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -112,48 +112,51 @@ class Linkedin extends OAuth2 } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserID(string $accessToken):string + public function getUserID(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['id'])) { - return $user['id']; - } - - return ''; + return $user['id'] ?? ''; } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserEmail(string $accessToken):string + public function getUserEmail(string $accessToken): string { - $email = \json_decode($this->request('GET', 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))', ['Authorization: Bearer '.\urlencode($accessToken)]), true); + $email = \json_decode($this->request('GET', 'https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))', ['Authorization: Bearer ' . \urlencode($accessToken)]), true); - if ( - isset($email['elements']) && - isset($email['elements'][0]) && - isset($email['elements'][0]['handle~']) && - isset($email['elements'][0]['handle~']['emailAddress']) - ) { - return $email['elements'][0]['handle~']['emailAddress']; - } - - return ''; + return $email['elements'][0]['handle~']['emailAddress'] ?? ''; } /** - * @param $accessToken + * Check if the OAuth email is verified + * + * If present, the email is verified. This was verfied through a manual Linkedin sign up process + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $email = $this->getUserEmail($accessToken); + + return !empty($email); + } + + /** + * @param string $accessToken * * @return string */ - public function getUserName(string $accessToken):string + public function getUserName(string $accessToken): string { $user = $this->getUser($accessToken); $name = ''; @@ -163,7 +166,7 @@ class Linkedin extends OAuth2 } if (isset($user['localizedLastName'])) { - $name = (empty($name)) ? $user['localizedLastName'] : $name.' '.$user['localizedLastName']; + $name = (empty($name)) ? $user['localizedLastName'] : $name . ' ' . $user['localizedLastName']; } return $name; @@ -177,7 +180,7 @@ class Linkedin extends OAuth2 protected function getUser(string $accessToken) { if (empty($this->user)) { - $this->user = \json_decode($this->request('GET', 'https://api.linkedin.com/v2/me', ['Authorization: Bearer '.\urlencode($accessToken)]), true); + $this->user = \json_decode($this->request('GET', 'https://api.linkedin.com/v2/me', ['Authorization: Bearer ' . \urlencode($accessToken)]), true); } return $this->user; diff --git a/src/Appwrite/Auth/OAuth2/Microsoft.php b/src/Appwrite/Auth/OAuth2/Microsoft.php index ebfd2e4e83..84f128a08e 100644 --- a/src/Appwrite/Auth/OAuth2/Microsoft.php +++ b/src/Appwrite/Auth/OAuth2/Microsoft.php @@ -13,17 +13,17 @@ class Microsoft extends OAuth2 /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = [ + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = [ 'offline_access', 'user.read' ]; @@ -35,17 +35,17 @@ class Microsoft extends OAuth2 { return 'microsoft'; } - + /** * @return string */ public function getLoginURL(): string { - return 'https://login.microsoftonline.com/'.$this->getTenantID().'/oauth2/v2.0/authorize?'.\http_build_query([ + return 'https://login.microsoftonline.com/' . $this->getTenantID() . '/oauth2/v2.0/authorize?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, - 'state'=> \json_encode($this->state), - 'scope'=> \implode(' ', $this->getScopes()), + 'state' => \json_encode($this->state), + 'scope' => \implode(' ', $this->getScopes()), 'response_type' => 'code', 'response_mode' => 'query' ]); @@ -58,7 +58,7 @@ class Microsoft extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', @@ -83,7 +83,7 @@ class Microsoft extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( @@ -98,7 +98,7 @@ class Microsoft extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -114,11 +114,7 @@ class Microsoft extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['id'])) { - return $user['id']; - } - - return ''; + return $user['id'] ?? ''; } /** @@ -130,11 +126,23 @@ class Microsoft extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['userPrincipalName'])) { - return $user['userPrincipalName']; - } + return $user['userPrincipalName'] ?? ''; + } - return ''; + /** + * Check if the OAuth email is verified + * + * If present, the email is verified. This was verfied through a manual Microsoft sign up process + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $email = $this->getUserEmail($accessToken); + + return !empty($email); } /** @@ -146,11 +154,7 @@ class Microsoft extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['displayName'])) { - return $user['displayName']; - } - - return ''; + return $user['displayName'] ?? ''; } /** @@ -161,7 +165,7 @@ class Microsoft extends OAuth2 protected function getUser(string $accessToken): array { if (empty($this->user)) { - $headers = ['Authorization: Bearer '. \urlencode($accessToken)]; + $headers = ['Authorization: Bearer ' . \urlencode($accessToken)]; $user = $this->request('GET', 'https://graph.microsoft.com/v1.0/me', $headers); $this->user = \json_decode($user, true); } @@ -175,7 +179,7 @@ class Microsoft extends OAuth2 * @return array */ protected function getAppSecret(): array - { + { try { $secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR); } catch (\Throwable $th) { @@ -192,7 +196,8 @@ class Microsoft extends OAuth2 protected function getClientSecret(): string { $secret = $this->getAppSecret(); - return (isset($secret['clientSecret'])) ? $secret['clientSecret'] : ''; + + return $secret['clientSecret'] ?? ''; } /** @@ -203,6 +208,7 @@ class Microsoft extends OAuth2 protected function getTenantID(): string { $secret = $this->getAppSecret(); - return (isset($secret['tenantID'])) ? $secret['tenantID'] : 'common'; + + return $secret['tenantID'] ?? 'common'; } } diff --git a/src/Appwrite/Auth/OAuth2/Mock.php b/src/Appwrite/Auth/OAuth2/Mock.php index aef92dac23..f80287947e 100644 --- a/src/Appwrite/Auth/OAuth2/Mock.php +++ b/src/Appwrite/Auth/OAuth2/Mock.php @@ -10,29 +10,29 @@ class Mock extends OAuth2 /** * @var string */ - protected $version = 'v1'; + protected string $version = 'v1'; /** * @var array */ - protected $scopes = [ + protected array $scopes = [ 'email' ]; /** * @var array */ - protected $user = []; - + protected array $user = []; + /** * @var array */ - protected $tokens = []; + protected array $tokens = []; /** * @return string */ - public function getName():string + public function getName(): string { return 'mock'; } @@ -40,9 +40,9 @@ class Mock extends OAuth2 /** * @return string */ - public function getLoginURL():string + public function getLoginURL(): string { - return 'http://localhost/'.$this->version.'/mock/tests/general/oauth2?'. \http_build_query([ + return 'http://localhost/' . $this->version . '/mock/tests/general/oauth2?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, 'scope' => \implode(' ', $this->getScopes()), @@ -57,16 +57,16 @@ class Mock extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $this->tokens = \json_decode($this->request( 'GET', 'http://localhost/' . $this->version . '/mock/tests/general/oauth2/token?' . - \http_build_query([ - 'client_id' => $this->appID, - 'redirect_uri' => $this->callback, - 'client_secret' => $this->appSecret, - 'code' => $code - ]) + \http_build_query([ + 'client_id' => $this->appID, + 'redirect_uri' => $this->callback, + 'client_secret' => $this->appSecret, + 'code' => $code + ]) ), true); } @@ -78,20 +78,20 @@ class Mock extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $this->tokens = \json_decode($this->request( 'GET', 'http://localhost/' . $this->version . '/mock/tests/general/oauth2/token?' . - \http_build_query([ - 'client_id' => $this->appID, - 'client_secret' => $this->appSecret, - 'refresh_token' => $refreshToken, - 'grant_type' => 'refresh_token' - ]) + \http_build_query([ + 'client_id' => $this->appID, + 'client_secret' => $this->appSecret, + 'refresh_token' => $refreshToken, + 'grant_type' => 'refresh_token' + ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -103,15 +103,11 @@ class Mock extends OAuth2 * * @return string */ - public function getUserID(string $accessToken):string + public function getUserID(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['id'])) { - return $user['id']; - } - - return ''; + return $user['id'] ?? ''; } /** @@ -119,15 +115,23 @@ class Mock extends OAuth2 * * @return string */ - public function getUserEmail(string $accessToken):string + public function getUserEmail(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['email'])) { - return $user['email']; - } + return $user['email'] ?? ''; + } - return ''; + /** + * Check if the OAuth email is verified + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + return true; } /** @@ -135,15 +139,11 @@ class Mock extends OAuth2 * * @return string */ - public function getUserName(string $accessToken):string + public function getUserName(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['name'])) { - return $user['name']; - } - - return ''; + return $user['name'] ?? ''; } /** @@ -151,10 +151,10 @@ class Mock extends OAuth2 * * @return array */ - protected function getUser(string $accessToken):array + protected function getUser(string $accessToken): array { if (empty($this->user)) { - $user = $this->request('GET', 'http://localhost/'.$this->version.'/mock/tests/general/oauth2/user?token='.\urlencode($accessToken)); + $user = $this->request('GET', 'http://localhost/' . $this->version . '/mock/tests/general/oauth2/user?token=' . \urlencode($accessToken)); $this->user = \json_decode($user, true); } diff --git a/src/Appwrite/Auth/OAuth2/Notion.php b/src/Appwrite/Auth/OAuth2/Notion.php index 5c117caa3c..5d9d75bffd 100644 --- a/src/Appwrite/Auth/OAuth2/Notion.php +++ b/src/Appwrite/Auth/OAuth2/Notion.php @@ -9,32 +9,32 @@ class Notion extends OAuth2 /** * @var string */ - private $endpoint = 'https://api.notion.com/v1'; + private string $endpoint = 'https://api.notion.com/v1'; /** * @var string */ - private $version = '2021-08-16'; + private string $version = '2021-08-16'; /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = []; + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = []; /** * @return string */ - public function getName():string + public function getName(): string { return 'notion'; } @@ -42,9 +42,9 @@ class Notion extends OAuth2 /** * @return string */ - public function getLoginURL():string + public function getLoginURL(): string { - return $this->endpoint . '/oauth/authorize?'. \http_build_query([ + return $this->endpoint . '/oauth/authorize?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, 'response_type' => 'code', @@ -60,7 +60,7 @@ class Notion extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)]; $this->tokens = \json_decode($this->request( 'POST', @@ -82,7 +82,7 @@ class Notion extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)]; $this->tokens = \json_decode($this->request( @@ -95,7 +95,7 @@ class Notion extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -103,51 +103,55 @@ class Notion extends OAuth2 } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserID(string $accessToken):string + public function getUserID(string $accessToken): string { $response = $this->getUser($accessToken); - if (isset($response['bot']['owner']['user']['id'])) { - return $response['bot']['owner']['user']['id']; - } - - return ''; + return $response['bot']['owner']['user']['id'] ?? ''; } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserEmail(string $accessToken):string + public function getUserEmail(string $accessToken): string { $response = $this->getUser($accessToken); - if(isset($response['bot']['owner']['user']['person']['email'])){ - return $response['bot']['owner']['user']['person']['email']; - } - - return ''; + return $response['bot']['owner']['user']['person']['email'] ?? ''; } /** - * @param $accessToken + * Check if the OAuth email is verified + * + * If present, the email is verified. This was verfied through a manual Notion sign up process + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $email = $this->getUserEmail($accessToken); + + return !empty($email); + } + + /** + * @param string $accessToken * * @return string */ - public function getUserName(string $accessToken):string + public function getUserName(string $accessToken): string { $response = $this->getUser($accessToken); - if (isset($response['bot']['owner']['user']['name'])) { - return $response['bot']['owner']['user']['name']; - } - - return ''; + return $response['bot']['owner']['user']['name'] ?? ''; } /** @@ -155,11 +159,11 @@ class Notion extends OAuth2 * * @return array */ - protected function getUser(string $accessToken) + protected function getUser(string $accessToken): array { $headers = [ 'Notion-Version: ' . $this->version, - 'Authorization: Bearer '.\urlencode($accessToken) + 'Authorization: Bearer ' . \urlencode($accessToken) ]; if (empty($this->user)) { diff --git a/src/Appwrite/Auth/OAuth2/Okta.php b/src/Appwrite/Auth/OAuth2/Okta.php index 7b1b0d19e1..3de3df96d5 100644 --- a/src/Appwrite/Auth/OAuth2/Okta.php +++ b/src/Appwrite/Auth/OAuth2/Okta.php @@ -8,27 +8,27 @@ use Appwrite\Auth\OAuth2; // https://developer.okta.com/docs/guides/sign-into-web-app-redirect/php/main/ class Okta extends OAuth2 -{ - /** +{ + /** * @var array */ - protected $scopes = [ + protected array $scopes = [ 'openid', 'profile', 'email', 'offline_access' ]; - + /** * @var array */ - protected $user = []; - + protected array $user = []; + /** * @var array */ - protected $tokens = []; - + protected array $tokens = []; + /** * @return string */ @@ -42,11 +42,11 @@ class Okta extends OAuth2 */ public function getLoginURL(): string { - return 'https://'.$this->getOktaDomain().'/oauth2/'.$this->getAuthorizationServerId().'/v1/authorize?'.\http_build_query([ + return 'https://' . $this->getOktaDomain() . '/oauth2/' . $this->getAuthorizationServerId() . '/v1/authorize?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, - 'state'=> \json_encode($this->state), - 'scope'=> \implode(' ', $this->getScopes()), + 'state' => \json_encode($this->state), + 'scope' => \implode(' ', $this->getScopes()), 'response_type' => 'code' ]); } @@ -58,11 +58,11 @@ class Okta extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', - 'https://'.$this->getOktaDomain().'/oauth2/'.$this->getAuthorizationServerId().'/v1/token', + 'https://' . $this->getOktaDomain() . '/oauth2/' . $this->getAuthorizationServerId() . '/v1/token', $headers, \http_build_query([ 'code' => $code, @@ -77,8 +77,8 @@ class Okta extends OAuth2 return $this->tokens; } - - + + /** * @param string $refreshToken * @@ -89,7 +89,7 @@ class Okta extends OAuth2 $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', - 'https://'.$this->getOktaDomain().'/oauth2/'.$this->getAuthorizationServerId().'/v1/token', + 'https://' . $this->getOktaDomain() . '/oauth2/' . $this->getAuthorizationServerId() . '/v1/token', $headers, \http_build_query([ 'refresh_token' => $refreshToken, @@ -99,7 +99,7 @@ class Okta extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -114,12 +114,8 @@ class Okta extends OAuth2 public function getUserID(string $accessToken): string { $user = $this->getUser($accessToken); - - if (isset($user['sub'])) { - return $user['sub']; - } - - return ''; + + return $user['sub'] ?? ''; } /** @@ -130,12 +126,28 @@ class Okta extends OAuth2 public function getUserEmail(string $accessToken): string { $user = $this->getUser($accessToken); - - if (isset($user['email'])) { - return $user['email']; + + return $user['email'] ?? ''; + } + + /** + * Check if the OAuth email is verified + * + * @link https://developer.okta.com/docs/reference/api/oidc/#userinfo + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if ($user['email_verified'] ?? false) { + return true; } - - return ''; + + return false; } /** @@ -146,15 +158,11 @@ class Okta extends OAuth2 public function getUserName(string $accessToken): string { $user = $this->getUser($accessToken); - - if (isset($user['name'])) { - return $user['name']; - } - - return ''; + + return $user['name'] ?? ''; } - - /** + + /** * @param string $accessToken * * @return array @@ -162,8 +170,8 @@ class Okta extends OAuth2 protected function getUser(string $accessToken): array { if (empty($this->user)) { - $headers = ['Authorization: Bearer '. \urlencode($accessToken)]; - $user = $this->request('GET', 'https://'.$this->getOktaDomain().'/oauth2/'.$this->getAuthorizationServerId().'/v1/userinfo', $headers); + $headers = ['Authorization: Bearer ' . \urlencode($accessToken)]; + $user = $this->request('GET', 'https://' . $this->getOktaDomain() . '/oauth2/' . $this->getAuthorizationServerId() . '/v1/userinfo', $headers); $this->user = \json_decode($user, true); } @@ -179,10 +187,10 @@ class Okta extends OAuth2 { $secret = $this->getAppSecret(); - return (isset($secret['clientSecret'])) ? $secret['clientSecret'] : ''; + return $secret['clientSecret'] ?? ''; } - /** + /** * Extracts the Okta Domain from the JSON stored in appSecret * * @return string @@ -190,7 +198,8 @@ class Okta extends OAuth2 protected function getOktaDomain(): string { $secret = $this->getAppSecret(); - return (isset($secret['oktaDomain'])) ? $secret['oktaDomain'] : ''; + + return $secret['oktaDomain'] ?? ''; } /** @@ -201,7 +210,8 @@ class Okta extends OAuth2 protected function getAuthorizationServerId(): string { $secret = $this->getAppSecret(); - return (isset($secret['authorizationServerId'])) ? $secret['authorizationServerId'] : 'default'; + + return $secret['authorizationServerId'] ?? 'default'; } /** @@ -210,7 +220,7 @@ class Okta extends OAuth2 * @return array */ protected function getAppSecret(): array - { + { try { $secret = \json_decode($this->appSecret, true, 512, JSON_THROW_ON_ERROR); } catch (\Throwable $th) { diff --git a/src/Appwrite/Auth/OAuth2/Paypal.php b/src/Appwrite/Auth/OAuth2/Paypal.php index 39544c6bd8..74f4291594 100644 --- a/src/Appwrite/Auth/OAuth2/Paypal.php +++ b/src/Appwrite/Auth/OAuth2/Paypal.php @@ -12,7 +12,7 @@ class Paypal extends OAuth2 /** * @var array */ - private $endpoint = [ + private array $endpoint = [ 'sandbox' => 'https://www.sandbox.paypal.com/', 'live' => 'https://www.paypal.com/', ]; @@ -20,7 +20,7 @@ class Paypal extends OAuth2 /** * @var array */ - private $resourceEndpoint = [ + private array $resourceEndpoint = [ 'sandbox' => 'https://api.sandbox.paypal.com/v1/', 'live' => 'https://api.paypal.com/v1/', ]; @@ -28,22 +28,22 @@ class Paypal extends OAuth2 /** * @var string */ - protected $environment = 'live'; + protected string $environment = 'live'; /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = [ + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = [ 'openid', 'profile', 'email' @@ -62,7 +62,7 @@ class Paypal extends OAuth2 */ public function getLoginURL(): string { - $url = $this->endpoint[$this->environment] . 'connect/?'. + $url = $this->endpoint[$this->environment] . 'connect/?' . \http_build_query([ 'flowEntry' => 'static', 'response_type' => 'code', @@ -83,7 +83,7 @@ class Paypal extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $this->tokens = \json_decode($this->request( 'POST', $this->resourceEndpoint[$this->environment] . 'oauth2/token', @@ -103,7 +103,7 @@ class Paypal extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $this->tokens = \json_decode($this->request( 'POST', @@ -115,7 +115,7 @@ class Paypal extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -131,11 +131,7 @@ class Paypal extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['payer_id'])) { - return $user['payer_id']; - } - - return ''; + return $user['payer_id'] ?? ''; } /** @@ -148,12 +144,38 @@ class Paypal extends OAuth2 $user = $this->getUser($accessToken); if (isset($user['emails'])) { - return $user['emails'][0]['value']; + $email = array_filter($user['emails'], function ($email) { + return $email['primary'] === true; + }); + + if (!empty($email)) { + return $email[0]['value']; + } } return ''; } + /** + * Check if the OAuth email is verified + * + * @link https://developer.paypal.com/docs/api/identity/v1/#userinfo_get + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if ($user['verified_account'] ?? false) { + return true; + } + + return false; + } + /** * @param string $accessToken * @@ -163,11 +185,7 @@ class Paypal extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['name'])) { - return $user['name']; - } - - return ''; + return $user['name'] ?? ''; } /** @@ -179,7 +197,7 @@ class Paypal extends OAuth2 { $header = [ 'Content-Type: application/json', - 'Authorization: Bearer '.\urlencode($accessToken), + 'Authorization: Bearer ' . \urlencode($accessToken), ]; if (empty($this->user)) { $user = $this->request( diff --git a/src/Appwrite/Auth/OAuth2/PaypalSandbox.php b/src/Appwrite/Auth/OAuth2/PaypalSandbox.php index 82698555d4..0f56a09c21 100644 --- a/src/Appwrite/Auth/OAuth2/PaypalSandbox.php +++ b/src/Appwrite/Auth/OAuth2/PaypalSandbox.php @@ -6,7 +6,7 @@ use Appwrite\Auth\OAuth2\Paypal; class PaypalSandbox extends Paypal { - protected $environment = 'sandbox'; + protected string $environment = 'sandbox'; /** * @return string diff --git a/src/Appwrite/Auth/OAuth2/Salesforce.php b/src/Appwrite/Auth/OAuth2/Salesforce.php index 04ef1901b5..636c3f4c3c 100644 --- a/src/Appwrite/Auth/OAuth2/Salesforce.php +++ b/src/Appwrite/Auth/OAuth2/Salesforce.php @@ -14,17 +14,17 @@ class Salesforce extends OAuth2 /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = [ + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = [ "openid" ]; @@ -37,7 +37,7 @@ class Salesforce extends OAuth2 } /** - * @param $state + * @param string $state * * @return array */ @@ -52,13 +52,13 @@ class Salesforce extends OAuth2 */ public function getLoginURL(): string { - return 'https://login.salesforce.com/services/oauth2/authorize?'.\http_build_query([ - 'response_type' => 'code', - 'client_id' => $this->appID, - 'redirect_uri'=> $this->callback, - 'scope'=> \implode(' ', $this->getScopes()), - 'state' => \json_encode($this->state) - ]); + return 'https://login.salesforce.com/services/oauth2/authorize?' . \http_build_query([ + 'response_type' => 'code', + 'client_id' => $this->appID, + 'redirect_uri' => $this->callback, + 'scope' => \implode(' ', $this->getScopes()), + 'state' => \json_encode($this->state) + ]); } /** @@ -68,7 +68,7 @@ class Salesforce extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = [ 'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret), 'Content-Type: application/x-www-form-urlencoded', @@ -93,7 +93,7 @@ class Salesforce extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $headers = [ 'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret), @@ -109,7 +109,7 @@ class Salesforce extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -125,11 +125,7 @@ class Salesforce extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['user_id'])) { - return $user['user_id']; - } - - return ''; + return $user['user_id'] ?? ''; } /** @@ -141,11 +137,27 @@ class Salesforce extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['email'])) { - return $user['email']; + return $user['email'] ?? ''; + } + + /** + * Check if the OAuth email is verified + * + * @link https://help.salesforce.com/s/articleView?id=sf.remoteaccess_using_userinfo_endpoint.htm&type=5 + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if ($user['email_verified'] ?? false) { + return true; } - return ''; + return false; } /** @@ -157,11 +169,7 @@ class Salesforce extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['name'])) { - return $user['name']; - } - - return ''; + return $user['name'] ?? ''; } /** @@ -172,7 +180,7 @@ class Salesforce extends OAuth2 protected function getUser(string $accessToken): array { if (empty($this->user)) { - $user = $this->request('GET', 'https://login.salesforce.com/services/oauth2/userinfo?access_token='.\urlencode($accessToken)); + $user = $this->request('GET', 'https://login.salesforce.com/services/oauth2/userinfo?access_token=' . \urlencode($accessToken)); $this->user = \json_decode($user, true); } return $this->user; diff --git a/src/Appwrite/Auth/OAuth2/Slack.php b/src/Appwrite/Auth/OAuth2/Slack.php index 92c210151b..c8adfdd697 100644 --- a/src/Appwrite/Auth/OAuth2/Slack.php +++ b/src/Appwrite/Auth/OAuth2/Slack.php @@ -3,24 +3,23 @@ namespace Appwrite\Auth\OAuth2; use Appwrite\Auth\OAuth2; -use Utopia\Exception; class Slack extends OAuth2 { /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = [ + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = [ 'identity.avatar', 'identity.basic', 'identity.email', @@ -30,7 +29,7 @@ class Slack extends OAuth2 /** * @return string */ - public function getName():string + public function getName(): string { return 'slack'; } @@ -38,11 +37,11 @@ class Slack extends OAuth2 /** * @return string */ - public function getLoginURL():string + public function getLoginURL(): string { // https://api.slack.com/docs/oauth#step_1_-_sending_users_to_authorize_and_or_install - return 'https://slack.com/oauth/authorize?'.\http_build_query([ - 'client_id'=> $this->appID, + return 'https://slack.com/oauth/authorize?' . \http_build_query([ + 'client_id' => $this->appID, 'scope' => \implode(' ', $this->getScopes()), 'redirect_uri' => $this->callback, 'state' => \json_encode($this->state) @@ -56,7 +55,7 @@ class Slack extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { // https://api.slack.com/docs/oauth#step_3_-_exchanging_a_verification_code_for_an_access_token $this->tokens = \json_decode($this->request( 'GET', @@ -77,7 +76,7 @@ class Slack extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $this->tokens = \json_decode($this->request( 'GET', @@ -89,7 +88,7 @@ class Slack extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -101,15 +100,11 @@ class Slack extends OAuth2 * * @return string */ - public function getUserID(string $accessToken):string + public function getUserID(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['user']['id'])) { - return $user['user']['id']; - } - - return ''; + return $user['user']['id'] ?? ''; } /** @@ -117,15 +112,29 @@ class Slack extends OAuth2 * * @return string */ - public function getUserEmail(string $accessToken):string + public function getUserEmail(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['user']['email'])) { - return $user['user']['email']; - } + return $user['user']['email'] ?? ''; + } - return ''; + /** + * Check if the OAuth email is verified + * + * If present, the email is verified. This was verfied through a manual Slack sign up process + * + * @link https://slack.com/help/articles/207262907-Change-your-email-address + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $email = $this->getUserEmail($accessToken); + + return !empty($email); } /** @@ -133,29 +142,26 @@ class Slack extends OAuth2 * * @return string */ - public function getUserName(string $accessToken):string + public function getUserName(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['user']['name'])) { - return $user['user']['name']; - } - - return ''; + return $user['user']['name'] ?? ''; } /** + * @link https://api.slack.com/methods/users.identity + * * @param string $accessToken * * @return array */ - protected function getUser(string $accessToken):array + protected function getUser(string $accessToken): array { if (empty($this->user)) { - // https://api.slack.com/methods/users.identity $user = $this->request( 'GET', - 'https://slack.com/api/users.identity?token='.\urlencode($accessToken) + 'https://slack.com/api/users.identity?token=' . \urlencode($accessToken) ); $this->user = \json_decode($user, true); diff --git a/src/Appwrite/Auth/OAuth2/Spotify.php b/src/Appwrite/Auth/OAuth2/Spotify.php index 5c1a43299a..98f4226d84 100644 --- a/src/Appwrite/Auth/OAuth2/Spotify.php +++ b/src/Appwrite/Auth/OAuth2/Spotify.php @@ -13,34 +13,34 @@ class Spotify extends OAuth2 /** * @var string */ - private $endpoint = 'https://accounts.spotify.com/'; + private string $endpoint = 'https://accounts.spotify.com/'; /** * @var string */ - private $resourceEndpoint = 'https://api.spotify.com/v1/'; + private string $resourceEndpoint = 'https://api.spotify.com/v1/'; /** * @var array */ - protected $scopes = [ + protected array $scopes = [ 'user-read-email', ]; /** * @var array */ - protected $user = []; - + protected array $user = []; + /** * @var array */ - protected $tokens = []; + protected array $tokens = []; /** * @return string */ - public function getName():string + public function getName(): string { return 'spotify'; } @@ -48,9 +48,9 @@ class Spotify extends OAuth2 /** * @return string */ - public function getLoginURL():string + public function getLoginURL(): string { - return $this->endpoint . 'authorize?'. + return $this->endpoint . 'authorize?' . \http_build_query([ 'response_type' => 'code', 'client_id' => $this->appID, @@ -67,7 +67,7 @@ class Spotify extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)]; $this->tokens = \json_decode($this->request( 'POST', @@ -89,7 +89,7 @@ class Spotify extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret)]; $this->tokens = \json_decode($this->request( @@ -102,7 +102,7 @@ class Spotify extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -110,51 +110,55 @@ class Spotify extends OAuth2 } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserID(string $accessToken):string + public function getUserID(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['id'])) { - return $user['id']; - } - - return ''; + return $user['id'] ?? ''; } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserEmail(string $accessToken):string + public function getUserEmail(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['email'])) { - return $user['email']; - } - - return ''; + return $user['email'] ?? ''; } /** - * @param $accessToken + * Check if the OAuth email is verified + * + * Spotify does not assure that the email is verified + * + * @link https://developer.spotify.com/documentation/web-api/reference/#/operations/get-current-users-profile + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + return false; + } + + /** + * @param string $accessToken * * @return string */ - public function getUserName(string $accessToken):string + public function getUserName(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['display_name'])) { - return $user['display_name']; - } - - return ''; + return $user['display_name'] ?? ''; } /** @@ -167,8 +171,8 @@ class Spotify extends OAuth2 if (empty($this->user)) { $this->user = \json_decode($this->request( 'GET', - $this->resourceEndpoint . "me", - ['Authorization: Bearer '.\urlencode($accessToken)] + $this->resourceEndpoint . 'me', + ['Authorization: Bearer ' . \urlencode($accessToken)] ), true); } diff --git a/src/Appwrite/Auth/OAuth2/Stripe.php b/src/Appwrite/Auth/OAuth2/Stripe.php index 589e6b7b81..e2ca3a92ba 100644 --- a/src/Appwrite/Auth/OAuth2/Stripe.php +++ b/src/Appwrite/Auth/OAuth2/Stripe.php @@ -10,38 +10,37 @@ class Stripe extends OAuth2 /** * @var array */ - protected $user = []; - + protected array $user = []; + /** * @var array */ - protected $tokens = []; + protected array $tokens = []; /** * @var string */ - protected $stripeAccountId = ''; + protected string $stripeAccountId = ''; /** * @var array */ - protected $scopes = [ + protected array $scopes = [ 'read_write', ]; - /** - * @return string + /** + * @var array */ - - protected $grantType = [ - 'authorize' => 'authorization_code', - 'refresh' => 'refresh_token', + protected array $grantType = [ + 'authorize' => 'authorization_code', + 'refresh' => 'refresh_token', ]; /** * @return string */ - public function getName():string + public function getName(): string { return 'stripe'; } @@ -49,9 +48,9 @@ class Stripe extends OAuth2 /** * @return string */ - public function getLoginURL():string + public function getLoginURL(): string { - return 'https://connect.stripe.com/oauth/authorize?'. \http_build_query([ + return 'https://connect.stripe.com/oauth/authorize?' . \http_build_query([ 'response_type' => 'code', // The only option at the moment is "code." 'client_id' => $this->appID, 'redirect_uri' => $this->callback, @@ -67,7 +66,7 @@ class Stripe extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $this->tokens = \json_decode($this->request( 'POST', 'https://connect.stripe.com/oauth/token', @@ -89,7 +88,7 @@ class Stripe extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $this->tokens = \json_decode($this->request( 'POST', @@ -101,7 +100,7 @@ class Stripe extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -110,51 +109,59 @@ class Stripe extends OAuth2 } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserID(string $accessToken):string + public function getUserID(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['id'])) { - return $user['id']; - } - - return ''; + return $user['id'] ?? ''; } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserEmail(string $accessToken):string + public function getUserEmail(string $accessToken): string { $user = $this->getUser($accessToken); - - if(empty($user)) { - return ''; + + if (empty($user)) { + return ''; } return $user['email'] ?? ''; } /** - * @param $accessToken + * Check if the OAuth email is verified + * + * If present, the email is verified. This was verfied through a manual Stripe sign up process + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $email = $this->getUserEmail($accessToken); + + return !empty($email); + } + + /** + * @param string $accessToken * * @return string */ - public function getUserName(string $accessToken):string + public function getUserName(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['name'])) { - return $user['name']; - } - - return ''; + return $user['name'] ?? ''; } /** @@ -166,15 +173,13 @@ class Stripe extends OAuth2 { if (empty($this->user) && !empty($this->stripeAccountId)) { $this->user = \json_decode( - $this->request( - 'GET', - 'https://api.stripe.com/v1/accounts/' . $this->stripeAccountId, - ['Authorization: Bearer '.\urlencode($accessToken)] - ), - true + $this->request( + 'GET', + 'https://api.stripe.com/v1/accounts/' . $this->stripeAccountId, + ['Authorization: Bearer ' . \urlencode($accessToken)] + ), + true ); - - } return $this->user; diff --git a/src/Appwrite/Auth/OAuth2/Tradeshift.php b/src/Appwrite/Auth/OAuth2/Tradeshift.php index 38b58432a0..8fdcde5f29 100644 --- a/src/Appwrite/Auth/OAuth2/Tradeshift.php +++ b/src/Appwrite/Auth/OAuth2/Tradeshift.php @@ -12,35 +12,37 @@ class Tradeshift extends OAuth2 const TRADESHIFT_SANDBOX_API_DOMAIN = 'api-sandbox.tradeshift.com'; const TRADESHIFT_API_DOMAIN = 'api.tradeshift.com'; - private $apiDomain = [ + private array $apiDomain = [ 'sandbox' => self::TRADESHIFT_SANDBOX_API_DOMAIN, 'live' => self::TRADESHIFT_API_DOMAIN, ]; - private $endpoint = [ + private array $endpoint = [ 'sandbox' => 'https://' . self::TRADESHIFT_SANDBOX_API_DOMAIN . '/tradeshift/', 'live' => 'https://' . self::TRADESHIFT_API_DOMAIN . '/tradeshift/', ]; - private $resourceEndpoint = [ + private array $resourceEndpoint = [ 'sandbox' => 'https://' . self::TRADESHIFT_SANDBOX_API_DOMAIN . '/tradeshift/rest/external/', 'live' => 'https://' . self::TRADESHIFT_API_DOMAIN . '/tradeshift/rest/external/', ]; - protected $environment = 'live'; + protected string $environment = 'live'; /** * @var array */ - protected $user = []; - + protected array $user = []; + /** * @var array */ - protected $tokens = []; + protected array $tokens = []; - - protected $scopes = [ + /** + * @var array + */ + protected array$scopes = [ 'openid', 'offline', ]; @@ -78,7 +80,7 @@ class Tradeshift extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $this->tokens = \json_decode($this->request( 'POST', $this->endpoint[$this->environment] . 'auth/token', @@ -98,7 +100,7 @@ class Tradeshift extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $this->tokens = \json_decode($this->request( 'POST', @@ -109,8 +111,8 @@ class Tradeshift extends OAuth2 'refresh_token' => $refreshToken, ]) ), true); - - if(empty($this->tokens['refresh_token'])) { + + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -141,6 +143,22 @@ class Tradeshift extends OAuth2 return $user['Username'] ?? ''; } + /** + * Check if the OAuth email is verified + * + * If present, the email is verified. This was verfied through a manual Tradeshift sign up process + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $email = $this->getUser($accessToken); + + return !empty($email); + } + /** * @param string $accessToken * diff --git a/src/Appwrite/Auth/OAuth2/TradeshiftBox.php b/src/Appwrite/Auth/OAuth2/TradeshiftBox.php index 6ba3c29f0a..27a4c0a456 100644 --- a/src/Appwrite/Auth/OAuth2/TradeshiftBox.php +++ b/src/Appwrite/Auth/OAuth2/TradeshiftBox.php @@ -6,7 +6,7 @@ use Appwrite\Auth\OAuth2\Tradeshift; class TradeshiftBox extends Tradeshift { - protected $environment = 'sandbox'; + protected string $environment = 'sandbox'; /** * @return string diff --git a/src/Appwrite/Auth/OAuth2/Twitch.php b/src/Appwrite/Auth/OAuth2/Twitch.php index 0deeb088e2..04e542ffb9 100644 --- a/src/Appwrite/Auth/OAuth2/Twitch.php +++ b/src/Appwrite/Auth/OAuth2/Twitch.php @@ -13,34 +13,34 @@ class Twitch extends OAuth2 /** * @var string */ - private $endpoint = 'https://id.twitch.tv/oauth2/'; + private string $endpoint = 'https://id.twitch.tv/oauth2/'; /** * @var string */ - private $resourceEndpoint = 'https://api.twitch.tv/helix/users'; + private string $resourceEndpoint = 'https://api.twitch.tv/helix/users'; /** * @var array */ - protected $scopes = [ + protected array $scopes = [ 'user:read:email', ]; /** * @var array */ - protected $user = []; - + protected array $user = []; + /** * @var array */ - protected $tokens = []; + protected array $tokens = []; /** * @return string */ - public function getName():string + public function getName(): string { return 'twitch'; } @@ -48,9 +48,9 @@ class Twitch extends OAuth2 /** * @return string */ - public function getLoginURL():string + public function getLoginURL(): string { - return $this->endpoint . 'authorize?'. + return $this->endpoint . 'authorize?' . \http_build_query([ 'response_type' => 'code', 'client_id' => $this->appID, @@ -68,7 +68,7 @@ class Twitch extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $this->tokens = \json_decode($this->request( 'POST', $this->endpoint . 'token?' . \http_build_query([ @@ -89,7 +89,7 @@ class Twitch extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $this->tokens = \json_decode($this->request( 'POST', @@ -101,7 +101,7 @@ class Twitch extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -109,51 +109,57 @@ class Twitch extends OAuth2 } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserID(string $accessToken):string + public function getUserID(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['id'])) { - return $user['id']; - } - - return ''; + return $user['id'] ?? ''; } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserEmail(string $accessToken):string + public function getUserEmail(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['email'])) { - return $user['email']; - } - - return ''; + return $user['email'] ?? ''; } /** - * @param $accessToken + * Check if the OAuth email is verified + * + * If present, the email is verified + * + * @link https://dev.twitch.tv/docs/api/reference#get-users + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $email = $this->getUserEmail($accessToken); + + return !empty($email); + } + + /** + * @param string $accessToken * * @return string */ - public function getUserName(string $accessToken):string + public function getUserName(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['display_name'])) { - return $user['display_name']; - } - - return ''; + return $user['display_name'] ?? ''; } /** @@ -168,8 +174,8 @@ class Twitch extends OAuth2 'GET', $this->resourceEndpoint, [ - 'Authorization: Bearer '.\urlencode($accessToken), - 'Client-Id: '. \urlencode($this->appID) + 'Authorization: Bearer ' . \urlencode($accessToken), + 'Client-Id: ' . \urlencode($this->appID) ] ), true); diff --git a/src/Appwrite/Auth/OAuth2/Vk.php b/src/Appwrite/Auth/OAuth2/Vk.php deleted file mode 100644 index 5e54b14394..0000000000 --- a/src/Appwrite/Auth/OAuth2/Vk.php +++ /dev/null @@ -1,191 +0,0 @@ - $this->appID, - 'redirect_uri' => $this->callback, - 'response_type' => 'code', - 'state' => \json_encode($this->state), - 'v' => $this->version, - 'scope' => \implode(' ', $this->getScopes()) - ]); - } - - /** - * @param string $code - * - * @return array - */ - protected function getTokens(string $code): array - { - if(empty($this->tokens)) { - $headers = ['Content-Type: application/x-www-form-urlencoded;charset=UTF-8']; - $this->tokens = \json_decode($this->request( - 'POST', - 'https://oauth.vk.com/access_token?', - $headers, - \http_build_query([ - 'code' => $code, - 'client_id' => $this->appID, - 'client_secret' => $this->appSecret, - 'redirect_uri' => $this->callback - ]) - ), true); - - $this->user['email'] = $this->tokens['email']; - $this->user['user_id'] = $this->tokens['user_id']; - } - - return $this->tokens; - } - - /** - * @param string $refreshToken - * - * @return array - */ - public function refreshTokens(string $refreshToken):array - { - $headers = ['Content-Type: application/x-www-form-urlencoded;charset=UTF-8']; - $this->tokens = \json_decode($this->request( - 'POST', - 'https://oauth.vk.com/access_token?', - $headers, - \http_build_query([ - 'refresh_token' => $refreshToken, - 'client_id' => $this->appID, - 'client_secret' => $this->appSecret, - 'grant_type' => 'refresh_token' - ]) - ), true); - - if(empty($this->tokens['refresh_token'])) { - $this->tokens['refresh_token'] = $refreshToken; - } - - $this->user['email'] = $this->tokens['email']; - $this->user['user_id'] = $this->tokens['user_id']; - - return $this->tokens; - } - - /** - * @param string $accessToken - * - * @return string - */ - public function getUserID(string $accessToken): string - { - $user = $this->getUser($accessToken); - - if (isset($user['user_id'])) { - return $user['user_id']; - } - - return ''; - } - - /** - * @param string $accessToken - * - * @return string - */ - public function getUserEmail(string $accessToken): string - { - $user = $this->getUser($accessToken); - - if (isset($user['email'])) { - return $user['email']; - } - - return ''; - } - - /** - * @param string $accessToken - * - * @return string - */ - public function getUserName(string $accessToken): string - { - $user = $this->getUser($accessToken); - - if (isset($user['name'])) { - return $user['name']; - } - - return ''; - } - - /** - * @param string $accessToken - * - * @return array - */ - protected function getUser(string $accessToken): array - { - if (empty($this->user['name'])) { - $user = $this->request( - 'GET', - 'https://api.vk.com/method/users.get?'. \http_build_query([ - 'v' => $this->version, - 'fields' => 'id,name,email,first_name,last_name', - 'access_token' => $accessToken - ]) - ); - - $user = \json_decode($user, true); - $this->user['name'] = $user['response'][0]['first_name'] ." ".$user['response'][0]['last_name']; - } - return $this->user; - } -} diff --git a/src/Appwrite/Auth/OAuth2/WordPress.php b/src/Appwrite/Auth/OAuth2/WordPress.php index 5b957d9495..6c1aade1de 100644 --- a/src/Appwrite/Auth/OAuth2/WordPress.php +++ b/src/Appwrite/Auth/OAuth2/WordPress.php @@ -12,24 +12,24 @@ class WordPress extends OAuth2 /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = [ + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = [ 'auth', ]; /** * @return string */ - public function getName():string + public function getName(): string { return 'wordpress'; } @@ -37,9 +37,9 @@ class WordPress extends OAuth2 /** * @return string */ - public function getLoginURL():string + public function getLoginURL(): string { - return 'https://public-api.wordpress.com/oauth2/authorize?'. \http_build_query([ + return 'https://public-api.wordpress.com/oauth2/authorize?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, 'response_type' => 'code', @@ -55,7 +55,7 @@ class WordPress extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $this->tokens = \json_decode($this->request( 'POST', 'https://public-api.wordpress.com/oauth2/token', @@ -78,7 +78,7 @@ class WordPress extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $this->tokens = \json_decode($this->request( 'POST', @@ -92,7 +92,7 @@ class WordPress extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -100,51 +100,63 @@ class WordPress extends OAuth2 } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserID(string $accessToken):string + public function getUserID(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['ID'])) { - return $user['ID']; + return $user['ID'] ?? ''; + } + + /** + * @param string $accessToken + * + * @return string + */ + public function getUserEmail(string $accessToken): string + { + $user = $this->getUser($accessToken); + + if ($user['verified']) { + return $user['email'] ?? ''; } return ''; } /** - * @param $accessToken - * - * @return string + * Check if the OAuth email is verified + * + * @link https://developer.wordpress.com/docs/api/1.1/get/me/ + * + * @param string $accessToken + * + * @return bool */ - public function getUserEmail(string $accessToken):string + public function isEmailVerified(string $accessToken): bool { $user = $this->getUser($accessToken); - if (isset($user['email']) && $user['verified']) { - return $user['email']; + if ($user['email_verified'] ?? false) { + return true; } - return ''; + return false; } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserName(string $accessToken):string + public function getUserName(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['username'])) { - return $user['username']; - } - - return ''; + return $user['username'] ?? ''; } /** @@ -155,7 +167,7 @@ class WordPress extends OAuth2 protected function getUser(string $accessToken) { if (empty($this->user)) { - $this->user = \json_decode($this->request('GET', 'https://public-api.wordpress.com/rest/v1/me', ['Authorization: Bearer '.$accessToken]), true); + $this->user = \json_decode($this->request('GET', 'https://public-api.wordpress.com/rest/v1/me', ['Authorization: Bearer ' . $accessToken]), true); } return $this->user; diff --git a/src/Appwrite/Auth/OAuth2/Yahoo.php b/src/Appwrite/Auth/OAuth2/Yahoo.php index ffefbe9c63..d8abbbfd69 100644 --- a/src/Appwrite/Auth/OAuth2/Yahoo.php +++ b/src/Appwrite/Auth/OAuth2/Yahoo.php @@ -13,17 +13,17 @@ class Yahoo extends OAuth2 /** * @var string */ - private $endpoint = 'https://api.login.yahoo.com/oauth2/'; + private string $endpoint = 'https://api.login.yahoo.com/oauth2/'; /** * @var string */ - private $resourceEndpoint = 'https://api.login.yahoo.com/openid/v1/userinfo'; + private string $resourceEndpoint = 'https://api.login.yahoo.com/openid/v1/userinfo'; /** * @var array */ - protected $scopes = [ + protected array $scopes = [ 'sdct-r', 'sdpp-w', ]; @@ -31,17 +31,17 @@ class Yahoo extends OAuth2 /** * @var array */ - protected $user = []; - + protected array $user = []; + /** * @var array */ - protected $tokens = []; + protected array $tokens = []; /** * @return string */ - public function getName():string + public function getName(): string { return 'yahoo'; } @@ -60,9 +60,9 @@ class Yahoo extends OAuth2 /** * @return string */ - public function getLoginURL():string + public function getLoginURL(): string { - return $this->endpoint . 'request_auth?'. + return $this->endpoint . 'request_auth?' . \http_build_query([ 'response_type' => 'code', 'client_id' => $this->appID, @@ -79,7 +79,7 @@ class Yahoo extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = [ 'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret), 'Content-Type: application/x-www-form-urlencoded', @@ -105,7 +105,7 @@ class Yahoo extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $headers = [ 'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret), @@ -122,7 +122,7 @@ class Yahoo extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -130,51 +130,55 @@ class Yahoo extends OAuth2 } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserID(string $accessToken):string + public function getUserID(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['sub'])) { - return $user['sub']; - } - - return ''; + return $user['sub'] ?? ''; } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserEmail(string $accessToken):string + public function getUserEmail(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['email'])) { - return $user['email']; - } - - return ''; + return $user['email'] ?? ''; } /** - * @param $accessToken + * Check if the OAuth email is verified + * + * If present, the email is verified. This was verfied through a manual Yahoo sign up process + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $email = $this->getUserEmail($accessToken); + + return !empty($email); + } + + /** + * @param string $accessToken * * @return string */ - public function getUserName(string $accessToken):string + public function getUserName(string $accessToken): string { $user = $this->getUser($accessToken); - if (isset($user['name'])) { - return $user['name']; - } - - return ''; + return $user['name'] ?? ''; } /** @@ -188,7 +192,7 @@ class Yahoo extends OAuth2 $this->user = \json_decode($this->request( 'GET', $this->resourceEndpoint, - ['Authorization: Bearer '.\urlencode($accessToken)] + ['Authorization: Bearer ' . \urlencode($accessToken)] ), true); } diff --git a/src/Appwrite/Auth/OAuth2/Yammer.php b/src/Appwrite/Auth/OAuth2/Yammer.php index 6e9f4fb86b..80bd44b244 100644 --- a/src/Appwrite/Auth/OAuth2/Yammer.php +++ b/src/Appwrite/Auth/OAuth2/Yammer.php @@ -12,17 +12,17 @@ class Yammer extends OAuth2 /** * @var string */ - private $endpoint = 'https://www.yammer.com/oauth2/'; + private string $endpoint = 'https://www.yammer.com/oauth2/'; /** * @var array */ - protected $user = []; - + protected array $user = []; + /** * @var array */ - protected $tokens = []; + protected array $tokens = []; /** * @return string @@ -37,13 +37,13 @@ class Yammer extends OAuth2 */ public function getLoginURL(): string { - return $this->endpoint . 'oauth2/authorize?'. - \http_build_query([ - 'client_id' => $this->appID, - 'response_type' => 'code', - 'redirect_uri' => $this->callback, - 'state' => \json_encode($this->state) - ]); + return $this->endpoint . 'oauth2/authorize?' . + \http_build_query([ + 'client_id' => $this->appID, + 'response_type' => 'code', + 'redirect_uri' => $this->callback, + 'state' => \json_encode($this->state) + ]); } /** @@ -53,7 +53,7 @@ class Yammer extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', @@ -76,7 +76,7 @@ class Yammer extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $headers = ['Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( @@ -91,7 +91,7 @@ class Yammer extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -107,11 +107,7 @@ class Yammer extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['id'])) { - return $user['id']; - } - - return ''; + return $user['id'] ?? ''; } /** @@ -123,11 +119,23 @@ class Yammer extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['email'])) { - return $user['email']; - } - - return ''; + return $user['email'] ?? ''; + } + + /** + * Check if the OAuth email is verified + * + * If present, the email is verified. This was verfied through a manual Yammer sign up process + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $email = $this->getUserEmail($accessToken); + + return !empty($email); } /** @@ -139,11 +147,7 @@ class Yammer extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['full_name'])) { - return $user['full_name']; - } - - return ''; + return $user['full_name'] ?? ''; } /** @@ -154,7 +158,7 @@ class Yammer extends OAuth2 protected function getUser(string $accessToken): array { if (empty($this->user)) { - $headers = ['Authorization: Bearer '. \urlencode($accessToken)]; + $headers = ['Authorization: Bearer ' . \urlencode($accessToken)]; $user = $this->request('GET', 'https://www.yammer.com/api/v1/users/current.json', $headers); $this->user = \json_decode($user, true); } diff --git a/src/Appwrite/Auth/OAuth2/Yandex.php b/src/Appwrite/Auth/OAuth2/Yandex.php index 1f6dda0d1b..efab408275 100644 --- a/src/Appwrite/Auth/OAuth2/Yandex.php +++ b/src/Appwrite/Auth/OAuth2/Yandex.php @@ -14,17 +14,17 @@ class Yandex extends OAuth2 /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = []; + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = []; /** * @return string @@ -35,7 +35,7 @@ class Yandex extends OAuth2 } /** - * @param $state + * @param string $state * * @return array */ @@ -50,12 +50,12 @@ class Yandex extends OAuth2 */ public function getLoginURL(): string { - return 'https://oauth.yandex.com/authorize?'.\http_build_query([ - 'response_type' => 'code', - 'client_id' => $this->appID, - 'scope'=> \implode(' ', $this->getScopes()), - 'state' => \json_encode($this->state) - ]); + return 'https://oauth.yandex.com/authorize?' . \http_build_query([ + 'response_type' => 'code', + 'client_id' => $this->appID, + 'scope' => \implode(' ', $this->getScopes()), + 'state' => \json_encode($this->state) + ]); } /** @@ -65,7 +65,7 @@ class Yandex extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = [ 'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret), 'Content-Type: application/x-www-form-urlencoded', @@ -89,7 +89,7 @@ class Yandex extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $headers = [ 'Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret), @@ -105,7 +105,7 @@ class Yandex extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -121,11 +121,7 @@ class Yandex extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['id'])) { - return $user['id']; - } - - return ''; + return $user['id'] ?? ''; } /** @@ -137,11 +133,19 @@ class Yandex extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['default_email'])) { - return $user['default_email']; - } + return $user['default_email'] ?? ''; + } - return ''; + /** + * Check if the OAuth email is verified + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + return false; } /** @@ -153,11 +157,7 @@ class Yandex extends OAuth2 { $user = $this->getUser($accessToken); - if (isset($user['display_name'])) { - return $user['display_name']; - } - - return ''; + return $user['display_name'] ?? ''; } /** @@ -168,7 +168,7 @@ class Yandex extends OAuth2 protected function getUser(string $accessToken): array { if (empty($this->user)) { - $user = $this->request('GET', 'https://login.yandex.ru/info?'.\http_build_query([ + $user = $this->request('GET', 'https://login.yandex.ru/info?' . \http_build_query([ 'format' => 'json', 'oauth_token' => $accessToken ])); diff --git a/src/Appwrite/Auth/OAuth2/Zoom.php b/src/Appwrite/Auth/OAuth2/Zoom.php index 9aa77ceb91..ee4185934c 100644 --- a/src/Appwrite/Auth/OAuth2/Zoom.php +++ b/src/Appwrite/Auth/OAuth2/Zoom.php @@ -9,34 +9,34 @@ class Zoom extends OAuth2 /** * @var string */ - private $endpoint = 'https://zoom.us'; + private string $endpoint = 'https://zoom.us'; /** * @var string */ - private $version = '2022-03-26'; + private string $version = '2022-03-26'; /** * @var array */ - protected $user = []; - - /** - * @var array - */ - protected $tokens = []; + protected array $user = []; /** * @var array */ - protected $scopes = [ + protected array $tokens = []; + + /** + * @var array + */ + protected array $scopes = [ 'user_profile' ]; /** * @return string */ - public function getName():string + public function getName(): string { return 'zoom'; } @@ -44,9 +44,9 @@ class Zoom extends OAuth2 /** * @return string */ - public function getLoginURL():string + public function getLoginURL(): string { - return $this->endpoint . '/oauth/authorize?'. \http_build_query([ + return $this->endpoint . '/oauth/authorize?' . \http_build_query([ 'client_id' => $this->appID, 'redirect_uri' => $this->callback, 'response_type' => 'code', @@ -62,7 +62,7 @@ class Zoom extends OAuth2 */ protected function getTokens(string $code): array { - if(empty($this->tokens)) { + if (empty($this->tokens)) { $headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret), 'Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( 'POST', @@ -84,7 +84,7 @@ class Zoom extends OAuth2 * * @return array */ - public function refreshTokens(string $refreshToken):array + public function refreshTokens(string $refreshToken): array { $headers = ['Authorization: Basic ' . \base64_encode($this->appID . ':' . $this->appSecret), 'Content-Type: application/x-www-form-urlencoded']; $this->tokens = \json_decode($this->request( @@ -97,7 +97,7 @@ class Zoom extends OAuth2 ]) ), true); - if(empty($this->tokens['refresh_token'])) { + if (empty($this->tokens['refresh_token'])) { $this->tokens['refresh_token'] = $refreshToken; } @@ -105,35 +105,58 @@ class Zoom extends OAuth2 } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserID(string $accessToken):string + public function getUserID(string $accessToken): string { $response = $this->getUser($accessToken); + return $response['id'] ?? ''; } /** - * @param $accessToken + * @param string $accessToken * * @return string */ - public function getUserEmail(string $accessToken):string + public function getUserEmail(string $accessToken): string { $response = $this->getUser($accessToken); + return $response['email'] ?? ''; } /** - * @param $accessToken + * Check if the OAuth email is verified + * + * @link https://marketplace.zoom.us/docs/api-reference/zoom-api/methods/#operation/user + * + * @param string $accessToken + * + * @return bool + */ + public function isEmailVerified(string $accessToken): bool + { + $user = $this->getUser($accessToken); + + if (($user['verified'] ?? false) === 1) { + return true; + } + + return false; + } + + /** + * @param string $accessToken * * @return string */ - public function getUserName(string $accessToken):string + public function getUserName(string $accessToken): string { $response = $this->getUser($accessToken); + return ($response['first_name'] ?? '') . ' ' . ($response['last_name'] ?? ''); } @@ -145,7 +168,7 @@ class Zoom extends OAuth2 protected function getUser(string $accessToken) { $headers = [ - 'Authorization: Bearer '.\urlencode($accessToken) + 'Authorization: Bearer ' . \urlencode($accessToken) ]; if (empty($this->user)) { diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 1099190cb6..95c51ed991 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -46,6 +46,7 @@ class Exception extends \Exception const GENERAL_ROUTE_NOT_FOUND = 'general_route_not_found'; const GENERAL_CURSOR_NOT_FOUND = 'general_cursor_not_found'; const GENERAL_SERVER_ERROR = 'general_server_error'; + const GENERAL_PROTOCOL_UNSUPPORTED = 'general_protocol_unsupported'; /** Users */ const USER_COUNT_EXCEEDED = 'user_count_exceeded'; diff --git a/src/Appwrite/Network/Validator/Host.php b/src/Appwrite/Network/Validator/Host.php index 703907c3d3..c81a931f37 100644 --- a/src/Appwrite/Network/Validator/Host.php +++ b/src/Appwrite/Network/Validator/Host.php @@ -2,6 +2,7 @@ namespace Appwrite\Network\Validator; +use Utopia\Validator\Hostname; use Utopia\Validator; /** @@ -45,17 +46,16 @@ class Host extends Validator */ public function isValid($value): bool { + // Check if value is valid URL $urlValidator = new URL(); if (!$urlValidator->isValid($value)) { return false; } - if (\in_array(\parse_url($value, PHP_URL_HOST), $this->whitelist)) { - return true; - } - - return false; + $hostname = \parse_url($value, PHP_URL_HOST); + $hostnameValidator = new Hostname($this->whitelist); + return $hostnameValidator->isValid($hostname); } /** diff --git a/src/Appwrite/Network/Validator/Origin.php b/src/Appwrite/Network/Validator/Origin.php index 8831707cef..30efe50c4c 100644 --- a/src/Appwrite/Network/Validator/Origin.php +++ b/src/Appwrite/Network/Validator/Origin.php @@ -2,6 +2,7 @@ namespace Appwrite\Network\Validator; +use Utopia\Validator\Hostname; use Utopia\Validator; class Origin extends Validator @@ -122,11 +123,9 @@ class Origin extends Validator return true; } - if (\in_array($host, $this->clients)) { - return true; - } - - return false; + $validator = new Hostname($this->clients); + + return $validator->isValid($host); } /** diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index 4796270d68..525c4144db 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -188,6 +188,24 @@ trait StorageBase $this->assertEquals('File extension not allowed', $res['body']['message']); return ['bucketId' => $bucketId, 'fileId' => $file['body']['$id'], 'largeFileId' => $largeFile['body']['$id'], 'largeBucketId' => $bucket2['body']['$id']]; + + /** + * Test for FAILURE create bucket with too high limit (bigger then _APP_STORAGE_LIMIT) + */ + $failedBucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'bucketId' => 'unique()', + 'name' => 'Test Bucket 2', + 'permission' => 'file', + 'maximumFileSize' => 200000000, //200MB + 'allowedFileExtensions' => ["jpg", "png"], + 'read' => ['role:all'], + 'write' => ['role:all'], + ]); + $this->assertEquals(400, $failedBucket['headers']['status-code']); } /** diff --git a/tests/e2e/Services/Teams/TeamsBaseServer.php b/tests/e2e/Services/Teams/TeamsBaseServer.php index 41dcc6c84c..5db4b628f6 100644 --- a/tests/e2e/Services/Teams/TeamsBaseServer.php +++ b/tests/e2e/Services/Teams/TeamsBaseServer.php @@ -204,7 +204,7 @@ trait TeamsBaseServer $this->assertEquals(1, $response['body']['total']); $this->assertIsInt($response['body']['total']); $this->assertIsInt($response['body']['dateCreated']); - + /** Delete User */ $user = $this->client->call(Client::METHOD_DELETE, '/users/' . $userUid, array_merge([