From b8f576b1d200254167286d1fae0001e5d6a3ed16 Mon Sep 17 00:00:00 2001 From: Thomas GASC Date: Tue, 31 Oct 2017 10:40:16 +0100 Subject: [PATCH 1/3] add feature that allows users to link their account through redirections between LPL and partners --- .gitignore | 1 + examples/liaison.php | 41 ++++++++++ src/Account/Account.php | 51 +++++++++++++ src/Account/Liaison.php | 121 +++++++++++++++++++++++++++++ src/Account/Repository.php | 27 +++++++ tests/Account/AccountTest.php | 39 ++++++++++ tests/Account/LiaisonTest.php | 139 ++++++++++++++++++++++++++++++++++ 7 files changed, 419 insertions(+) create mode 100644 examples/liaison.php create mode 100644 src/Account/Account.php create mode 100644 src/Account/Liaison.php create mode 100644 src/Account/Repository.php create mode 100644 tests/Account/AccountTest.php create mode 100644 tests/Account/LiaisonTest.php diff --git a/.gitignore b/.gitignore index 588b16e..26c8e4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /coverage/ /vendor/ /composer.lock +/phpunit.xml /examples/credentials.txt \ No newline at end of file diff --git a/examples/liaison.php b/examples/liaison.php new file mode 100644 index 0000000..74e73ae --- /dev/null +++ b/examples/liaison.php @@ -0,0 +1,41 @@ +accounts[$code]; + } + public function save(Account $account) + { + $this->accounts[$account->getCode()] = $account; + } +} + +$repository = new MemoryRepository([ + new Account('test1@domain.tld', '99f104e8-2fa3-4a77-1664-5bac75fb668d'), + new Account('test2@domain.tld', '68b3c837-c7f4-1b54-2efa-1c5cc2945c3f'), +]); +$logguedAccount = new Account('test3@domain.tld', '7f75e972-d5c7-b0c5-1a1b-9d5a582cbd27'); + +$liaison = new Liaison($encryption, $repository, $public_key); +$redirection = $liaison->generateUrl($_GET['lpluser'], $logguedAccount); + +header('Location: '.$redirection); diff --git a/src/Account/Account.php b/src/Account/Account.php new file mode 100644 index 0000000..61476f2 --- /dev/null +++ b/src/Account/Account.php @@ -0,0 +1,51 @@ + + * + * For the full license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mediapart\LaPresseLibre\Account; + +class Account +{ + /** + * @var string + */ + private $code; + + /** + * @var string + */ + private $email; + + /** + * @param string $email + * @param string $code + */ + public function __construct($email, $code = null) + { + $this->code = $code; + $this->email = $email; + } + + /** + * @return string|null + */ + public function getCode() + { + return $this->code; + } + + /** + * @return string + */ + public function getEmail() + { + return $this->email; + } +} diff --git a/src/Account/Liaison.php b/src/Account/Liaison.php new file mode 100644 index 0000000..5ff6358 --- /dev/null +++ b/src/Account/Liaison.php @@ -0,0 +1,121 @@ + + * + * For the full license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mediapart\LaPresseLibre\Account; + +use Mediapart\LaPresseLibre\Security\Encryption; +use Mediapart\LaPresseLibre\Account\Account; +use Mediapart\LaPresseLibre\Account\Repository; + +/** + * @see https://github.com/NextINpact/LaPresseLibreSDK/wiki/Liaison-de-compte-utilisateur-par-redirection + */ +class Liaison +{ + const RETURN_URL = 'https://beta.lapresselibre.fr/manage/link-result?lpl=%1$s&part=%2$s'; + const STATUS_SUCCESS = 1; + const STATUS_FAILURE = 2; + const STATUS_CONFLICT = 3; + + /** + * @var Encryption + */ + private $encryption; + + /** + * @var Repository + */ + private $repository; + + /** + * @var int + */ + private $public_key; + + /** + * @param Encryption $encryption + * @param Repository $repository + * @param int $public_key + */ + public function __construct(Encryption $encryption, Repository $repository, $public_key) + { + $this->encryption = $encryption; + $this->repository = $repository; + $this->public_key = $public_key; + } + + /** + * Liaison de compte utilisateur par redirection + * + * @param string $lplUser + * @param Account $logguedAccount + * @return string + */ + public function generateUrl($lplUser, Account $logguedAccount) + { + /* Le paramètre "lpluser" représente l'ID LPL de l'utilisateur qui + souhaite lier son compte. Il est chiffré en AES256 puis codé en + base64 en reprenant la méthode de chiffrement utilisée pour les + web services. */ + $code = $this->encryption->decrypt($lplUser); + + if ($existingAccount = $this->repository->find($code)) { + + /* En cas de conflit la valeur du statut que le partenaire doit + retourner sera "3". Sauf évidement s'il s'agit du bon compte + utilisateur. */ + $status = ($existingAccount != $logguedAccount) + ? self::STATUS_CONFLICT + : self::STATUS_SUCCESS + ; + + } else { + try { + + /* Si l'ID LPL reçu n'est pas déjà présent, le partenaire + doit rechercher le compte utilisateur pour y rattacher + L'ID LPL. Puis on retourne un statut "1" pour indiquer + que la liaison s'est effectuée avec succès. */ + $account = new Account($logguedAccount->getEmail(), $code); + $this->repository->save($account); + $status = self::STATUS_SUCCESS; + + } catch (\Exception $e) { + + /* Le statut retourné par le partenaire LPL est "2" en cas + d'erreur. */ + $status = self::STATUS_FAILURE; + } + } + + /* Le partenaire doit rediriger l'utilisateur vers l'url fournie par + LPL avec les paramètres : */ + return sprintf( + self::RETURN_URL, + + /* "lpl" : composé de l'ID LPL et du statut. Ce paramètre sera + ensuite chiffré en AES puis codé en base64. + Exemple : { Guid: xxxx, statut: 1 } */ + rawurlencode( + $this->encryption->encrypt( + [ + 'Guid' => $code, + 'statut' => $status, + ], + OPENSSL_RAW_DATA & OPENSSL_NO_PADDING + ) + ), + + /* "part" : qui représente le code du partenaire. */ + $this->public_key + ); + } +} diff --git a/src/Account/Repository.php b/src/Account/Repository.php new file mode 100644 index 0000000..2b9c114 --- /dev/null +++ b/src/Account/Repository.php @@ -0,0 +1,27 @@ + + * + * For the full license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mediapart\LaPresseLibre\Account; + +interface Repository +{ + /** + * @param string $code + * @return Account|null + */ + public function find($code); + + /** + * @param Account $account + * @return void + */ + public function save(Account $account); +} diff --git a/tests/Account/AccountTest.php b/tests/Account/AccountTest.php new file mode 100644 index 0000000..8636913 --- /dev/null +++ b/tests/Account/AccountTest.php @@ -0,0 +1,39 @@ + + * + * For the full license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mediapart\LaPresseLibre\Tests; + +use PHPUnit\Framework\TestCase; +use Mediapart\LaPresseLibre\Account\Account; + +class AccountTest extends TestCase +{ + public function testAccountWithoutCode() + { + $email = 'username@domain.tld'; + + $account = new Account($email); + + $this->assertEquals($email, $account->getEmail()); + $this->assertNull($account->getCode()); + } + + public function testAccountWithCode() + { + $email = 'username@domain.tld'; + $lplCode = '68b3c837-c7f4-1b54-2efa-1c5cc2945c3f'; + + $account = new Account($email, $lplCode); + + $this->assertEquals($email, $account->getEmail()); + $this->assertEquals($lplCode, $account->getCode()); + } +} diff --git a/tests/Account/LiaisonTest.php b/tests/Account/LiaisonTest.php new file mode 100644 index 0000000..6c388b2 --- /dev/null +++ b/tests/Account/LiaisonTest.php @@ -0,0 +1,139 @@ + + * + * For the full license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mediapart\LaPresseLibre\Tests; + +use PHPUnit\Framework\TestCase; +use Mediapart\LaPresseLibre\Security\Encryption; +use Mediapart\LaPresseLibre\Account\Liaison; +use Mediapart\LaPresseLibre\Account\Account; +use Mediapart\LaPresseLibre\Account\Repository; + +class LiaisonTest extends TestCase +{ + public function testSuccessWithExistingAccount() + { + $public_key = "2"; + $code = '99f104e8-2fa3-4a77-1664-5bac75fb668d'; + $encryption = $this->createMock(Encryption::class); + $repository = $this->createMock(Repository::class); + $logguedAccount = new Account('username@domain.tld', $code); + $repository + ->method('find') + ->with($code) + ->willReturn(clone $logguedAccount) + ; + $encryption + ->method('decrypt') + ->with('received data') + ->willReturn($code) + ; + $encryption + ->method('encrypt') + ->with(['Guid' => $code, 'statut' => Liaison::STATUS_SUCCESS]) + ->willReturn('response data') + ; + + $liaison = new Liaison($encryption, $repository, $public_key); + $link = $liaison->generateUrl('received data', $logguedAccount); + + $this->assertEquals('https://beta.lapresselibre.fr/manage/link-result?lpl=response%20data&part=2', $link); + } + + public function testConflict() + { + $public_key = "2"; + $code = '99f104e8-2fa3-4a77-1664-5bac75fb668d'; + $encryption = $this->createMock(Encryption::class); + $repository = $this->createMock(Repository::class); + $logguedAccount = new Account('username@domain.tld'); + $conflictedAccount = new Account('othername@domain.tld', $code); + $repository + ->method('find') + ->with($code) + ->willReturn($conflictedAccount) + ; + $encryption + ->method('decrypt') + ->with('received data') + ->willReturn($code) + ; + $encryption + ->method('encrypt') + ->with(['Guid' => $code, 'statut' => Liaison::STATUS_CONFLICT]) + ->willReturn('response data') + ; + + $liaison = new Liaison($encryption, $repository, $public_key); + $link = $liaison->generateUrl('received data', $logguedAccount); + + $this->assertEquals('https://beta.lapresselibre.fr/manage/link-result?lpl=response%20data&part=2', $link); + } + + public function testSuccessWithSavingNewLinkedAccount() + { + $public_key = "2"; + $code = '99f104e8-2fa3-4a77-1664-5bac75fb668d'; + $encryption = $this->createMock(Encryption::class); + $repository = $this->createMock(Repository::class); + $logguedAccount = new Account('username@domain.tld'); + $repository + ->expects($this->once()) + ->method('save') + ->with(new Account($logguedAccount->getEmail(), $code)) + ; + $encryption + ->method('decrypt') + ->with('received data') + ->willReturn($code) + ; + $encryption + ->method('encrypt') + ->with(['Guid' => $code, 'statut' => Liaison::STATUS_SUCCESS]) + ->willReturn('response data') + ; + + $liaison = new Liaison($encryption, $repository, $public_key); + $link = $liaison->generateUrl('received data', $logguedAccount); + + $this->assertEquals('https://beta.lapresselibre.fr/manage/link-result?lpl=response%20data&part=2', $link); + } + + public function testErrorWhenSavingNewLinkedAccount() + { + $public_key = "2"; + $code = '99f104e8-2fa3-4a77-1664-5bac75fb668d'; + $encryption = $this->createMock(Encryption::class); + $repository = $this->createMock(Repository::class); + $logguedAccount = new Account('username@domain.tld'); + $repository + ->expects($this->once()) + ->method('save') + ->with(new Account($logguedAccount->getEmail(), $code)) + ->will($this->throwException(new \Exception)); + ; + $encryption + ->method('decrypt') + ->with('received data') + ->willReturn($code) + ; + $encryption + ->method('encrypt') + ->with(['Guid' => $code, 'statut' => Liaison::STATUS_FAILURE]) + ->willReturn('response data') + ; + + $liaison = new Liaison($encryption, $repository, $public_key); + $link = $liaison->generateUrl('received data', $logguedAccount); + + $this->assertEquals('https://beta.lapresselibre.fr/manage/link-result?lpl=response%20data&part=2', $link); + } +} From 5d4ac398dbba8d755d7262248d5b46c61bfbfdf0 Mon Sep 17 00:00:00 2001 From: Thomas GASC Date: Tue, 31 Oct 2017 15:07:13 +0100 Subject: [PATCH 2/3] fix #discussion_r147937488 --- src/Account/{Liaison.php => Link.php} | 4 +-- .../Account/{LiaisonTest.php => LinkTest.php} | 36 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) rename src/Account/{Liaison.php => Link.php} (97%) rename tests/Account/{LiaisonTest.php => LinkTest.php} (76%) diff --git a/src/Account/Liaison.php b/src/Account/Link.php similarity index 97% rename from src/Account/Liaison.php rename to src/Account/Link.php index 5ff6358..1e37a97 100644 --- a/src/Account/Liaison.php +++ b/src/Account/Link.php @@ -18,7 +18,7 @@ /** * @see https://github.com/NextINpact/LaPresseLibreSDK/wiki/Liaison-de-compte-utilisateur-par-redirection */ -class Liaison +class Link { const RETURN_URL = 'https://beta.lapresselibre.fr/manage/link-result?lpl=%1$s&part=%2$s'; const STATUS_SUCCESS = 1; @@ -59,7 +59,7 @@ public function __construct(Encryption $encryption, Repository $repository, $pub * @param Account $logguedAccount * @return string */ - public function generateUrl($lplUser, Account $logguedAccount) + public function generate($lplUser, Account $logguedAccount) { /* Le paramètre "lpluser" représente l'ID LPL de l'utilisateur qui souhaite lier son compte. Il est chiffré en AES256 puis codé en diff --git a/tests/Account/LiaisonTest.php b/tests/Account/LinkTest.php similarity index 76% rename from tests/Account/LiaisonTest.php rename to tests/Account/LinkTest.php index 6c388b2..ece60af 100644 --- a/tests/Account/LiaisonTest.php +++ b/tests/Account/LinkTest.php @@ -13,11 +13,11 @@ use PHPUnit\Framework\TestCase; use Mediapart\LaPresseLibre\Security\Encryption; -use Mediapart\LaPresseLibre\Account\Liaison; +use Mediapart\LaPresseLibre\Account\Link; use Mediapart\LaPresseLibre\Account\Account; use Mediapart\LaPresseLibre\Account\Repository; -class LiaisonTest extends TestCase +class LinkTest extends TestCase { public function testSuccessWithExistingAccount() { @@ -38,14 +38,14 @@ public function testSuccessWithExistingAccount() ; $encryption ->method('encrypt') - ->with(['Guid' => $code, 'statut' => Liaison::STATUS_SUCCESS]) + ->with(['Guid' => $code, 'statut' => Link::STATUS_SUCCESS]) ->willReturn('response data') ; - $liaison = new Liaison($encryption, $repository, $public_key); - $link = $liaison->generateUrl('received data', $logguedAccount); + $link = new Link($encryption, $repository, $public_key); + $url = $link->generate('received data', $logguedAccount); - $this->assertEquals('https://beta.lapresselibre.fr/manage/link-result?lpl=response%20data&part=2', $link); + $this->assertEquals('https://beta.lapresselibre.fr/manage/link-result?lpl=response%20data&part=2', $url); } public function testConflict() @@ -68,14 +68,14 @@ public function testConflict() ; $encryption ->method('encrypt') - ->with(['Guid' => $code, 'statut' => Liaison::STATUS_CONFLICT]) + ->with(['Guid' => $code, 'statut' => Link::STATUS_CONFLICT]) ->willReturn('response data') ; - $liaison = new Liaison($encryption, $repository, $public_key); - $link = $liaison->generateUrl('received data', $logguedAccount); + $link = new Link($encryption, $repository, $public_key); + $url = $link->generate('received data', $logguedAccount); - $this->assertEquals('https://beta.lapresselibre.fr/manage/link-result?lpl=response%20data&part=2', $link); + $this->assertEquals('https://beta.lapresselibre.fr/manage/link-result?lpl=response%20data&part=2', $url); } public function testSuccessWithSavingNewLinkedAccount() @@ -97,14 +97,14 @@ public function testSuccessWithSavingNewLinkedAccount() ; $encryption ->method('encrypt') - ->with(['Guid' => $code, 'statut' => Liaison::STATUS_SUCCESS]) + ->with(['Guid' => $code, 'statut' => Link::STATUS_SUCCESS]) ->willReturn('response data') ; - $liaison = new Liaison($encryption, $repository, $public_key); - $link = $liaison->generateUrl('received data', $logguedAccount); + $link = new Link($encryption, $repository, $public_key); + $url = $link->generate('received data', $logguedAccount); - $this->assertEquals('https://beta.lapresselibre.fr/manage/link-result?lpl=response%20data&part=2', $link); + $this->assertEquals('https://beta.lapresselibre.fr/manage/link-result?lpl=response%20data&part=2', $url); } public function testErrorWhenSavingNewLinkedAccount() @@ -127,13 +127,13 @@ public function testErrorWhenSavingNewLinkedAccount() ; $encryption ->method('encrypt') - ->with(['Guid' => $code, 'statut' => Liaison::STATUS_FAILURE]) + ->with(['Guid' => $code, 'statut' => Link::STATUS_FAILURE]) ->willReturn('response data') ; - $liaison = new Liaison($encryption, $repository, $public_key); - $link = $liaison->generateUrl('received data', $logguedAccount); + $link = new Link($encryption, $repository, $public_key); + $url = $link->generate('received data', $logguedAccount); - $this->assertEquals('https://beta.lapresselibre.fr/manage/link-result?lpl=response%20data&part=2', $link); + $this->assertEquals('https://beta.lapresselibre.fr/manage/link-result?lpl=response%20data&part=2', $url); } } From 37b1f10c46e295984243686e7d546d991775e50b Mon Sep 17 00:00:00 2001 From: Thomas Gasc Date: Fri, 17 Nov 2017 09:22:02 +0100 Subject: [PATCH 3/3] Encryption can decrypt not json strings (#2) encryption can decrypt not json strings --- src/Security/Encryption.php | 5 ++++- tests/Security/EncryptionTest.php | 30 ++++++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/src/Security/Encryption.php b/src/Security/Encryption.php index 4d8c44b..32a5749 100644 --- a/src/Security/Encryption.php +++ b/src/Security/Encryption.php @@ -100,7 +100,10 @@ public function decrypt($message, $options = null) $options, $this->iv ); - $result = json_decode(rtrim($result, "\0"), true); + $result = rtrim($result, "\0"); + + $decodedJson = json_decode($result, true); + $result = null!==$decodedJson ? $decodedJson : $result; $this->logger->debug('Uncrypting message', [$message, $result]); diff --git a/tests/Security/EncryptionTest.php b/tests/Security/EncryptionTest.php index 7df7692..77e0f6c 100644 --- a/tests/Security/EncryptionTest.php +++ b/tests/Security/EncryptionTest.php @@ -16,14 +16,36 @@ class EncryptionTest extends TestCase { - public function testEncrypt() + public function testEncryption() { $string = 'lorem ipsum dolor'; - $encryption = new Encryption('passphrase'); + $encryption = new Encryption('passphrase', '8265408651542848', 0); $encrypted = $encryption->encrypt($string); - $decrypted = $encryption->decrypt($string); + $decrypted = $encryption->decrypt($encrypted); - $this->assertEquals($encrypted, $decrypted); + $this->assertEquals('UVZVTXBlNnBsSy9Ea2lsai8zRjZreElvbHAxeW0rVm1rcmRzNi9nQ2lKYz0=', $encrypted); + $this->assertEquals($string, $decrypted); + } + + public function testNotJsonStringDecryption() + { + $encrypted = 'VHZoeHhsdjgwV3RsODVVcEd5ak1MaWc3UlJFRnF6Ly9IemtWSUlWcjJlMD0='; + + $encryption = new Encryption('passphrase', '8265408651542848', 0); + $decrypted = $encryption->decrypt($encrypted); + + $this->assertEquals('lorem ipsum dolor', $decrypted); + } + + public function testIv() + { + $string = 'loremp ipsum dolor'; + + $encryption = new Encryption('passphrase', null, 0); + $encrypted = $encryption->encrypt($string); + + $this->assertNotEquals('', $encrypted); + $this->assertNotEquals($string, $encrypted); } }