From ec6e4f3a063f5cc4cd7b19132ef7cdbea6b71e36 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 14:46:16 +0100 Subject: [PATCH 01/14] adapt user collection properties --- app/config/collections.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/collections.php b/app/config/collections.php index 9170b0757..3307e7a12 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -204,7 +204,7 @@ $collections = [ 'key' => 'email', 'type' => Database::SYSTEM_VAR_TYPE_EMAIL, 'default' => '', - 'required' => true, + 'required' => false, 'array' => false, ], [ @@ -222,7 +222,7 @@ $collections = [ 'key' => 'password', 'type' => Database::SYSTEM_VAR_TYPE_TEXT, 'default' => '', - 'required' => true, + 'required' => false, 'array' => false, ], [ From 9d189165040fe956ef4aaa9dfcb119aa95faae39 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 14:46:30 +0100 Subject: [PATCH 02/14] create new endpoint --- app/controllers/api/account.php | 141 ++++++++++++++++++++++++++++++-- 1 file changed, 136 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index e8ae76312..bae62db4a 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -564,6 +564,128 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') ; }); +App::post('/v1/account/sessions/anonymous') + ->desc('Create Anonymous Account') + ->groups(['api', 'account']) + ->label('event', 'account.sessions.create') + ->label('scope', 'public') + ->label('sdk.platform', [APP_PLATFORM_CLIENT]) + ->label('sdk.namespace', 'account') + ->label('sdk.method', 'createAnonymous') + ->label('sdk.description', '/docs/references/account/create-anonymous.md') + ->label('sdk.response.code', Response::STATUS_CODE_CREATED) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_SESSION) + ->label('abuse-limit', 50) + ->label('abuse-key', 'ip:{ip}') + ->inject('request') + ->inject('response') + ->inject('locale') + ->inject('user') + ->inject('projectDB') + ->inject('geodb') + ->inject('audits') + ->action(function ($request, $response, $locale, $user, $projectDB, $geodb, $audits) { + /** @var Utopia\Swoole\Request $request */ + /** @var Appwrite\Utopia\Response $response */ + /** @var Utopia\Locale\Locale $locale */ + /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Database $projectDB */ + /** @var MaxMind\Db\Reader $geodb */ + /** @var Appwrite\Event\Event $audits */ + + $protocol = $request->getProtocol(); + + if ($user->getId()) { + throw new Exception('Failed to create anonymous user.', 401); + } + + Authorization::disable(); + try { + $user = $projectDB->createDocument([ + '$collection' => Database::SYSTEM_COLLECTION_USERS, + '$permissions' => [ + 'read' => ['*'], + 'write' => ['user:{self}'] + ], + 'email' => null, + 'emailVerification' => false, + 'status' => Auth::USER_STATUS_UNACTIVATED, + 'password' => null, + 'passwordUpdate' => \time(), + 'registration' => \time(), + 'reset' => false, + 'name' => null + ]); + } catch (Exception $th) { + throw new Exception('Failed saving user to DB', 500); + } + + Authorization::enable(); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + // 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(); + $expiry = \time() + Auth::TOKEN_EXPIRATION_LOGIN_LONG; + $session = new Document(array_merge( + [ + '$collection' => Database::SYSTEM_COLLECTION_TOKENS, + '$permissions' => ['read' => ['user:' . $user['$id']], 'write' => ['user:' . $user['$id']]], + 'userId' => $user->getId(), + 'type' => Auth::TOKEN_TYPE_LOGIN, + 'secret' => Auth::hash($secret), // One way hash encryption to protect DB leak + 'expire' => $expiry, + 'userAgent' => $request->getUserAgent('UNKNOWN'), + 'ip' => $request->getIP(), + 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', + ], + $detector->getOS(), + $detector->getClient(), + $detector->getDevice() + )); + + $user->setAttribute('tokens', $session, Document::SET_TYPE_APPEND); + + Authorization::setRole('user:'.$user->getId()); + + $user = $projectDB->updateDocument($user->getArrayCopy()); + + if (false === $user) { + throw new Exception('Failed saving user to DB', 500); + } + + $audits + ->setParam('userId', $user->getId()) + ->setParam('event', 'account.sessions.create') + ->setParam('resource', 'users/'.$user->getId()) + ; + + if (!Config::getParam('domainVerification')) { + $response + ->addHeader('X-Fallback-Cookies', \json_encode([Auth::$cookieName => Auth::encodeSession($user->getId(), $secret)])) + ; + } + + $response + ->addCookie(Auth::$cookieName.'_legacy', Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, null) + ->addCookie(Auth::$cookieName, Auth::encodeSession($user->getId(), $secret), $expiry, '/', Config::getParam('cookieDomain'), ('https' == $protocol), true, Config::getParam('cookieSamesite')) + ->setStatusCode(Response::STATUS_CODE_CREATED) + ; + + $session + ->setAttribute('current', true) + ->setAttribute('countryName', (isset($countries[$session->getAttribute('countryCode')])) ? $countries[$session->getAttribute('countryCode')] : $locale->getText('locale.country.unknown')) + ; + + $response->dynamic($session, Response::MODEL_SESSION); + }); + App::post('/v1/account/jwt') ->desc('Create Account JWT') ->groups(['api', 'account']) @@ -878,7 +1000,12 @@ App::patch('/v1/account/email') /** @var Appwrite\Database\Database $projectDB */ /** @var Appwrite\Event\Event $audits */ - if (!Auth::passwordVerify($password, $user->getAttribute('password'))) { // Double check user password + $isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); // Check if request is from an anonymous account for converting + + if ( + !$isAnonymousUser && + !Auth::passwordVerify($password, $user->getAttribute('password')) + ) { // Double check user password throw new Exception('Invalid credentials', 401); } @@ -896,10 +1023,14 @@ App::patch('/v1/account/email') // TODO after this user needs to confirm mail again - $user = $projectDB->updateDocument(\array_merge($user->getArrayCopy(), [ - 'email' => $email, - 'emailVerification' => false, - ])); + $user = $projectDB->updateDocument(\array_merge( + $user->getArrayCopy(), + ($isAnonymousUser ? [ 'password' => Auth::passwordHash($password) ] : []), + [ + 'email' => $email, + 'emailVerification' => false, + ] + )); if (false === $user) { throw new Exception('Failed saving user to DB', 500); From f199e70a8fc9552f0fc76b674d2b9e5432079adf Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 14:46:44 +0100 Subject: [PATCH 03/14] update docs --- docs/references/account/create-anonymous.md | 1 + docs/references/account/update-email.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 docs/references/account/create-anonymous.md diff --git a/docs/references/account/create-anonymous.md b/docs/references/account/create-anonymous.md new file mode 100644 index 000000000..61895604a --- /dev/null +++ b/docs/references/account/create-anonymous.md @@ -0,0 +1 @@ +Use this endpoint to allow a new user to register an anonymous account in your project. This route will also create a new session for the user. To allow the new user to convert an anonymous account to a normal account account, you need to update its [email and password](/docs/client/account#accountUpdateEmail). \ No newline at end of file diff --git a/docs/references/account/update-email.md b/docs/references/account/update-email.md index d99b9b4a1..7be36b7cd 100644 --- a/docs/references/account/update-email.md +++ b/docs/references/account/update-email.md @@ -1 +1,2 @@ -Update currently logged in user account email address. After changing user address, user confirmation status is being reset and a new confirmation mail is sent. For security measures, user password is required to complete this request. \ No newline at end of file +Update currently logged in user account email address. After changing user address, user confirmation status is being reset and a new confirmation mail is sent. For security measures, user password is required to complete this request. +This endpoint can also be used to convert an anonymous account to a normal one, by passing an email address and a new password. \ No newline at end of file From 6c717b7a314ac74ede2d69ebff0d43cb6d2379d2 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 14:49:21 +0100 Subject: [PATCH 04/14] add tests --- tests/e2e/Services/Account/AccountBase.php | 33 ++++++ .../Account/AccountCustomClientTest.php | 106 ++++++++++++++++++ 2 files changed, 139 insertions(+) diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index 5476e0455..b8aa1063a 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -49,6 +49,39 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 409); + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => '', + 'password' => '', + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => '', + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => '', + 'password' => $password, + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + return [ 'id' => $id, 'email' => $email, diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index a11165530..2a0eea743 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -226,4 +226,110 @@ class AccountCustomClientTest extends Scope return []; } + + public function testCreateAnonymousAccount() + { + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/anonymous', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]); + + $this->assertEquals($response['headers']['status-code'], 201); + + $session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/anonymous', [ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]); + + $this->assertEquals($response['headers']['status-code'], 401); + + return $session; + } + + /** + * @depends testCreateAnonymousAccount + */ + public function testUpdateAccountPassword($session):array + { + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]), [ + 'password' => 'new-password', + 'oldPassword' => null, + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + return []; + } + + /** + * @depends testCreateAnonymousAccount + */ + public function testConvertAnonymousAccount($session):array + { + $email = uniqid().'new@localhost.test'; + $password = 'new-password'; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertIsArray($response['body']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']); + $this->assertNotEmpty($response['body']['$id']); + $this->assertIsNumeric($response['body']['registration']); + $this->assertEquals($response['body']['email'], $email); + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ])); + + $this->assertEquals($response['headers']['status-code'], 401); + + $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]), [ + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + + return []; + } } \ No newline at end of file From 6005551023befdd556b8ca8056bf823a87ee31e8 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 15:14:36 +0100 Subject: [PATCH 05/14] update tests --- tests/e2e/Services/Account/AccountBase.php | 4 -- .../Account/AccountCustomClientTest.php | 53 ++++++++++--------- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index b8aa1063a..38d985ab0 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -405,7 +405,6 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 200); $this->assertIsArray($response['body']); $this->assertNotEmpty($response['body']); - $this->assertNotEmpty($response['body']); $this->assertNotEmpty($response['body']['$id']); $this->assertIsNumeric($response['body']['registration']); $this->assertEquals($response['body']['email'], $email); @@ -473,7 +472,6 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 200); $this->assertIsArray($response['body']); $this->assertNotEmpty($response['body']); - $this->assertNotEmpty($response['body']); $this->assertNotEmpty($response['body']['$id']); $this->assertIsNumeric($response['body']['registration']); $this->assertEquals($response['body']['email'], $email); @@ -540,7 +538,6 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 200); $this->assertIsArray($response['body']); $this->assertNotEmpty($response['body']); - $this->assertNotEmpty($response['body']); $this->assertNotEmpty($response['body']['$id']); $this->assertIsNumeric($response['body']['registration']); $this->assertEquals($response['body']['email'], $newEmail); @@ -598,7 +595,6 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 200); $this->assertIsArray($response['body']); $this->assertNotEmpty($response['body']); - $this->assertNotEmpty($response['body']); $this->assertEquals('prefValue1', $response['body']['prefs']['prefKey1']); $this->assertEquals('prefValue2', $response['body']['prefs']['prefKey2']); diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 2a0eea743..498037d3e 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -260,10 +260,10 @@ class AccountCustomClientTest extends Scope /** * @depends testCreateAnonymousAccount */ - public function testUpdateAccountPassword($session):array + public function testUpdateAnonymousAccountPassword($session):array { /** - * Test for SUCCESS + * Test for FAILURE */ $response = $this->client->call(Client::METHOD_PATCH, '/account/password', array_merge([ 'origin' => 'http://localhost', @@ -272,7 +272,7 @@ class AccountCustomClientTest extends Scope 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, ]), [ 'password' => 'new-password', - 'oldPassword' => null, + 'oldPassword' => '', ]); $this->assertEquals($response['headers']['status-code'], 400); @@ -280,6 +280,31 @@ class AccountCustomClientTest extends Scope return []; } + /** + * @depends testUpdateAnonymousAccountPassword + */ + public function testUpdateAnonymousAccountEmail($session):array + { + $email = uniqid().'new@localhost.test'; + + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]), [ + 'email' => $email, + 'password' => '', + ]); + + $this->assertEquals($response['headers']['status-code'], 401); + + return []; + } + /** * @depends testCreateAnonymousAccount */ @@ -304,32 +329,10 @@ class AccountCustomClientTest extends Scope $this->assertEquals($response['headers']['status-code'], 200); $this->assertIsArray($response['body']); $this->assertNotEmpty($response['body']); - $this->assertNotEmpty($response['body']); $this->assertNotEmpty($response['body']['$id']); $this->assertIsNumeric($response['body']['registration']); $this->assertEquals($response['body']['email'], $email); - /** - * Test for FAILURE - */ - $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ])); - - $this->assertEquals($response['headers']['status-code'], 401); - - $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ - 'origin' => 'http://localhost', - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, - ]), [ - ]); - - $this->assertEquals($response['headers']['status-code'], 400); - return []; } } \ No newline at end of file From c9b46d93dceba48739a76d2a4943769069c5f651 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 15:16:09 +0100 Subject: [PATCH 06/14] fix comment --- app/controllers/api/account.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index bae62db4a..5ab3e6e15 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -627,7 +627,7 @@ App::post('/v1/account/sessions/anonymous') throw new Exception('Failed saving user to DB', 500); } - // Create session token, verify user account and update OAuth2 ID and Access Token + // Create session token $detector = new Detector($request->getUserAgent('UNKNOWN')); $record = $geodb->get($request->getIP()); From b559f8068e5b33f5c52bd9f9ba362060ba572915 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 15:20:15 +0100 Subject: [PATCH 07/14] add test to login after converting --- .../e2e/Services/Account/AccountCustomClientTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 498037d3e..56dba168c 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -333,6 +333,17 @@ class AccountCustomClientTest extends Scope $this->assertIsNumeric($response['body']['registration']); $this->assertEquals($response['body']['email'], $email); + $response = $this->client->call(Client::METHOD_POST, '/account/sessions', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals($response['headers']['status-code'], 201); + return []; } } \ No newline at end of file From 6a31e19d731727f9099de9af30cf6ed0e084a138 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 16:01:15 +0100 Subject: [PATCH 08/14] add test to convert to existing email --- .../Account/AccountCustomClientTest.php | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 56dba168c..73b79e2c8 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -313,9 +313,35 @@ class AccountCustomClientTest extends Scope $email = uniqid().'new@localhost.test'; $password = 'new-password'; + /** + * Test for FAILURE + */ + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ]), [ + 'email' => $email, + 'password' => $password + ]); + + $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals($response['headers']['status-code'], 400); + /** * Test for SUCCESS */ + $email = uniqid().'new@localhost.test'; + $response = $this->client->call(Client::METHOD_PATCH, '/account/email', array_merge([ 'origin' => 'http://localhost', 'content-type' => 'application/json', From 1387fb50f108818c65f49532a0bff95608c613cc Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 16 Feb 2021 16:51:08 +0100 Subject: [PATCH 09/14] add tests for oauth2 --- app/controllers/api/account.php | 9 +++ .../Account/AccountCustomClientTest.php | 56 +++++++++++++++++-- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 5ab3e6e15..277f4e737 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -515,6 +515,15 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') 'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--', ], $detector->getOS(), $detector->getClient(), $detector->getDevice())); + $isAnonymousUser = is_null($user->getAttribute('email')) && is_null($user->getAttribute('password')); + + if ($isAnonymousUser) { + $user + ->setAttribute('name', $oauth2->getUserName($accessToken)) + ->setAttribute('email', $oauth2->getUserEmail($accessToken)) + ; + } + $user ->setAttribute('oauth2'.\ucfirst($provider), $oauth2ID) ->setAttribute('oauth2'.\ucfirst($provider).'AccessToken', $accessToken) diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index 73b79e2c8..cf1af8749 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -305,11 +305,9 @@ class AccountCustomClientTest extends Scope return []; } - /** - * @depends testCreateAnonymousAccount - */ - public function testConvertAnonymousAccount($session):array + public function testConvertAnonymousAccount():array { + $session = $this->testCreateAnonymousAccount(); $email = uniqid().'new@localhost.test'; $password = 'new-password'; @@ -372,4 +370,54 @@ class AccountCustomClientTest extends Scope return []; } + + public function testConvertAnonymousAccountOAuth2():array + { + $session = $this->testCreateAnonymousAccount(); + $provider = 'mock'; + $appId = '1'; + $secret = '123456'; + + /** + * Test for SUCCESS + */ + $response = $this->client->call(Client::METHOD_PATCH, '/projects/'.$this->getProject()['$id'].'/oauth2', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => 'console', + 'cookie' => 'a_session_console=' . $this->getRoot()['session'], + ]), [ + 'provider' => $provider, + 'appId' => $appId, + 'secret' => $secret, + ]); + + $this->assertEquals($response['headers']['status-code'], 200); + + $response = $this->client->call(Client::METHOD_GET, '/account/sessions/oauth2/'.$provider, array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ]), [ + 'success' => 'http://localhost/v1/mock/tests/general/oauth2/success', + 'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure', + ]); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals('success', $response['body']['result']); + + $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, + ])); + + $this->assertEquals($response['headers']['status-code'], 200); + $this->assertEquals($response['body']['name'], 'User Name'); + $this->assertEquals($response['body']['email'], 'user@localhost.test'); + + return []; + } } \ No newline at end of file From d1ae8aa15c90342207e5cbbed4714b91c71a7081 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 17 Feb 2021 09:47:07 +0100 Subject: [PATCH 10/14] adapt to review --- app/controllers/api/account.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 277f4e737..5c095c231 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -109,7 +109,7 @@ App::post('/v1/account') throw new Exception('Account already exists', 409); } - Authorization::enable(); + Authorization::reset(); Authorization::unsetRole('role:'.Auth::USER_ROLE_GUEST); Authorization::setRole('user:'.$user->getId()); @@ -485,7 +485,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') throw new Exception('Account already exists', 409); } - Authorization::enable(); + Authorization::reset(); if (false === $user) { throw new Exception('Failed saving user to DB', 500); @@ -630,7 +630,7 @@ App::post('/v1/account/sessions/anonymous') throw new Exception('Failed saving user to DB', 500); } - Authorization::enable(); + Authorization::reset(); if (false === $user) { throw new Exception('Failed saving user to DB', 500); From 8d3d3bca681f88b23195875e0c2a2d7aa1f61b4f Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Wed, 17 Feb 2021 10:38:45 +0100 Subject: [PATCH 11/14] added anon login to changelog --- CHANGES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 490007bcd..5b10ab43c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,9 @@ +# Version DEV (NOT RELEASED YET) + +## Features + +- Added Anonymous Login ([RFC-010](https://github.com/appwrite/rfc/blob/main/010-anonymous-login.md)) + # Version 0.7.0 ## Features From 2c1247d667b4b17fbc29c66254dd83ce712ff13d Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 18 Feb 2021 15:52:27 +0100 Subject: [PATCH 12/14] change sdk method name --- app/controllers/api/account.php | 6 +++--- .../{create-anonymous.md => create-session-anonymous.md} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename docs/references/account/{create-anonymous.md => create-session-anonymous.md} (100%) diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 5c095c231..d90118d90 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -574,14 +574,14 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect') }); App::post('/v1/account/sessions/anonymous') - ->desc('Create Anonymous Account') + ->desc('Create Anonymous Session') ->groups(['api', 'account']) ->label('event', 'account.sessions.create') ->label('scope', 'public') ->label('sdk.platform', [APP_PLATFORM_CLIENT]) ->label('sdk.namespace', 'account') - ->label('sdk.method', 'createAnonymous') - ->label('sdk.description', '/docs/references/account/create-anonymous.md') + ->label('sdk.method', 'createAnonymousSession') + ->label('sdk.description', '/docs/references/account/create-session-anonymous.md') ->label('sdk.response.code', Response::STATUS_CODE_CREATED) ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) ->label('sdk.response.model', Response::MODEL_SESSION) diff --git a/docs/references/account/create-anonymous.md b/docs/references/account/create-session-anonymous.md similarity index 100% rename from docs/references/account/create-anonymous.md rename to docs/references/account/create-session-anonymous.md From e829f8f7bcbb478f311e61f71e393f96874f6d33 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 23 Feb 2021 23:43:05 +0100 Subject: [PATCH 13/14] add env var and console prevention --- .env | 1 + CHANGES.md | 1 + app/config/variables.php | 8 ++++++++ app/controllers/api/account.php | 10 ++++++++-- app/views/install/compose.phtml | 1 + docker-compose.yml | 1 + tests/e2e/Services/Account/AccountCustomClientTest.php | 5 +++-- 7 files changed, 23 insertions(+), 4 deletions(-) diff --git a/.env b/.env index 581af6d97..0f8c2e196 100644 --- a/.env +++ b/.env @@ -39,3 +39,4 @@ _APP_MAINTENANCE_RETENTION_EXECUTION=1209600 _APP_MAINTENANCE_RETENTION_ABUSE=86400 _APP_MAINTENANCE_RETENTION_AUDIT=1209600 _APP_USAGE_STATS=enabled +_APP_LOGIN_ANONYMOUS=enabled diff --git a/CHANGES.md b/CHANGES.md index 5b10ab43c..a822b68e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ ## Features - Added Anonymous Login ([RFC-010](https://github.com/appwrite/rfc/blob/main/010-anonymous-login.md)) +- Added new Environment Variable to enable or disable Anonymous Login # Version 0.7.0 diff --git a/app/config/variables.php b/app/config/variables.php index e842283c2..62d9d302a 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -119,6 +119,14 @@ return [ 'required' => false, 'question' => '', ], + [ + 'name' => '_APP_LOGIN_ANONYMOUS', + 'description' => 'This variable allows you to enable anonymous login.', + 'introduction' => '0.8.0', + 'default' => 'enabled', + 'required' => false, + 'question' => '', + ], ], ], [ diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index d90118d90..6bd6dcdd9 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -591,21 +591,27 @@ App::post('/v1/account/sessions/anonymous') ->inject('response') ->inject('locale') ->inject('user') + ->inject('project') ->inject('projectDB') ->inject('geodb') ->inject('audits') - ->action(function ($request, $response, $locale, $user, $projectDB, $geodb, $audits) { + ->action(function ($request, $response, $locale, $user, $project, $projectDB, $geodb, $audits) { /** @var Utopia\Swoole\Request $request */ /** @var Appwrite\Utopia\Response $response */ /** @var Utopia\Locale\Locale $locale */ /** @var Appwrite\Database\Document $user */ + /** @var Appwrite\Database\Document $project */ /** @var Appwrite\Database\Database $projectDB */ /** @var MaxMind\Db\Reader $geodb */ /** @var Appwrite\Event\Event $audits */ $protocol = $request->getProtocol(); - if ($user->getId()) { + if(App::getEnv('_APP_LOGIN_ANONYMOUS', 'enabled') !== 'enabled') { + throw new Exception('Anonymous login is disabled.', 412); + } + + if ($user->getId() || 'console' === $project->getId()) { throw new Exception('Failed to create anonymous user.', 401); } diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index b98642fb9..daee965c5 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -89,6 +89,7 @@ services: - _APP_FUNCTIONS_MEMORY - _APP_FUNCTIONS_MEMORY_SWAP - _APP_FUNCTIONS_ENVS + - _APP_LOGIN_ANONYMOUS appwrite-worker-usage: image: appwrite/appwrite: diff --git a/docker-compose.yml b/docker-compose.yml index c682ac839..3b2260a20 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -109,6 +109,7 @@ services: - _APP_FUNCTIONS_MEMORY - _APP_FUNCTIONS_MEMORY_SWAP - _APP_FUNCTIONS_ENVS + - _APP_LOGIN_ANONYMOUS appwrite-worker-usage: entrypoint: worker-usage diff --git a/tests/e2e/Services/Account/AccountCustomClientTest.php b/tests/e2e/Services/Account/AccountCustomClientTest.php index cf1af8749..e1dedd6be 100644 --- a/tests/e2e/Services/Account/AccountCustomClientTest.php +++ b/tests/e2e/Services/Account/AccountCustomClientTest.php @@ -6,6 +6,7 @@ use Tests\E2E\Client; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\SideClient; +use Utopia\App; class AccountCustomClientTest extends Scope { @@ -238,7 +239,7 @@ class AccountCustomClientTest extends Scope 'x-appwrite-project' => $this->getProject()['$id'], ]); - $this->assertEquals($response['headers']['status-code'], 201); + $this->assertEquals(201, $response['headers']['status-code']); $session = $this->client->parseCookie((string)$response['headers']['set-cookie'])['a_session_'.$this->getProject()['$id']]; @@ -252,7 +253,7 @@ class AccountCustomClientTest extends Scope 'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $session, ]); - $this->assertEquals($response['headers']['status-code'], 401); + $this->assertEquals(401, $response['headers']['status-code']); return $session; } From b9017549c2a9c268ae49266102c257c4214c9b95 Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Tue, 23 Mar 2021 09:38:56 +0100 Subject: [PATCH 14/14] remove env variable in favor of #947 --- .env | 1 - app/config/variables.php | 10 +--------- app/controllers/api/account.php | 4 ---- app/views/install/compose.phtml | 1 - docker-compose.yml | 1 - 5 files changed, 1 insertion(+), 16 deletions(-) diff --git a/.env b/.env index 0f8c2e196..581af6d97 100644 --- a/.env +++ b/.env @@ -39,4 +39,3 @@ _APP_MAINTENANCE_RETENTION_EXECUTION=1209600 _APP_MAINTENANCE_RETENTION_ABUSE=86400 _APP_MAINTENANCE_RETENTION_AUDIT=1209600 _APP_USAGE_STATS=enabled -_APP_LOGIN_ANONYMOUS=enabled diff --git a/app/config/variables.php b/app/config/variables.php index c4a02b6c6..f061271d4 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -118,15 +118,7 @@ return [ 'default' => 'enabled', 'required' => false, 'question' => '', - ], - [ - 'name' => '_APP_LOGIN_ANONYMOUS', - 'description' => 'This variable allows you to enable anonymous login.', - 'introduction' => '0.8.0', - 'default' => 'enabled', - 'required' => false, - 'question' => '', - ], + ] ], ], [ diff --git a/app/controllers/api/account.php b/app/controllers/api/account.php index 6bd6dcdd9..6475f99b6 100644 --- a/app/controllers/api/account.php +++ b/app/controllers/api/account.php @@ -607,10 +607,6 @@ App::post('/v1/account/sessions/anonymous') $protocol = $request->getProtocol(); - if(App::getEnv('_APP_LOGIN_ANONYMOUS', 'enabled') !== 'enabled') { - throw new Exception('Anonymous login is disabled.', 412); - } - if ($user->getId() || 'console' === $project->getId()) { throw new Exception('Failed to create anonymous user.', 401); } diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index 0d7795762..1c0c98108 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -94,7 +94,6 @@ services: - _APP_FUNCTIONS_MEMORY - _APP_FUNCTIONS_MEMORY_SWAP - _APP_FUNCTIONS_ENVS - - _APP_LOGIN_ANONYMOUS appwrite-worker-usage: image: appwrite/appwrite: diff --git a/docker-compose.yml b/docker-compose.yml index 1cad9d2a9..c4ad29c64 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -111,7 +111,6 @@ services: - _APP_FUNCTIONS_MEMORY - _APP_FUNCTIONS_MEMORY_SWAP - _APP_FUNCTIONS_ENVS - - _APP_LOGIN_ANONYMOUS appwrite-worker-usage: entrypoint: worker-usage