From 379291a8c956b7be05fbf12f4060985938d48118 Mon Sep 17 00:00:00 2001 From: Tharanga Kothalawala Date: Mon, 25 Feb 2019 21:09:12 +0000 Subject: [PATCH 1/5] added Spotify support (#5) --- README.md | 2 + examples/README.md | 1 + examples/images/demo_before_login.PNG | Bin 38439 -> 39300 bytes examples/images/spotify.png | Bin 0 -> 16410 bytes examples/index.php | 4 +- examples/sso.php | 10 ++ src/ThirdParty.php | 1 + .../Spotify/SpotifyApiConfiguration.php | 75 +++++++++ src/ThirdParty/Spotify/SpotifyConnection.php | 142 ++++++++++++++++++ .../Spotify/SpotifyConnectionFactory.php | 38 +++++ .../Spotify/SpotifyApiConfigurationTest.php | 35 +++++ .../Spotify/SpotifyConnectionFactoryTest.php | 24 +++ 12 files changed, 331 insertions(+), 1 deletion(-) create mode 100644 examples/images/spotify.png create mode 100644 src/ThirdParty/Spotify/SpotifyApiConfiguration.php create mode 100644 src/ThirdParty/Spotify/SpotifyConnection.php create mode 100644 src/ThirdParty/Spotify/SpotifyConnectionFactory.php create mode 100644 tests/ThirdParty/Spotify/SpotifyApiConfigurationTest.php create mode 100644 tests/ThirdParty/Spotify/SpotifyConnectionFactoryTest.php diff --git a/README.md b/README.md index 4114e6b..78d2855 100755 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This is a library which can provision new accounts and can authenticate users ut * Google * LinkedIn * Slack +* Spotify * Twitter * Yahoo @@ -206,6 +207,7 @@ Optionally you may register your own apps if you want to test. * GitHub : https://github.com/settings/developers * Google : https://console.developers.google.com * Twitter : https://developer.twitter.com/en/apps - You must at least have 'Read-only' access permission and have ticked 'Request email address from users' under additional permissions. +* Spotify : https://developer.spotify.com/dashboard/applications * Yahoo : https://developer.yahoo.com/apps - You must at least select 'Read/Write Public and Private' of 'Profiles (Social Directory)' API permissions. #### Host File Entry diff --git a/examples/README.md b/examples/README.md index d6fbd1d..d154e32 100644 --- a/examples/README.md +++ b/examples/README.md @@ -9,6 +9,7 @@ Optionally you may register your own apps if you want to test. * GitHub : https://github.com/settings/developers * Google : https://console.developers.google.com * Twitter : https://developer.twitter.com/en/apps - You must at least have 'Read-only' access permission and have ticked 'Request email address from users' under additional permissions. +* Spotify : https://developer.spotify.com/dashboard/applications * Yahoo : https://developer.yahoo.com/apps - You must at least select 'Read/Write Public and Private' of 'Profiles (Social Directory)' API permissions. #### Host File Entry diff --git a/examples/images/demo_before_login.PNG b/examples/images/demo_before_login.PNG index 858ee0ed1bab8fc23a0f469fda9a832c0e88e863..e6ffaa7a913bce1ec947c6de6d8dc160f5ea47e2 100644 GIT binary patch literal 39300 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
    JPuqTZJyd3KcXh<9g* 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 L-Tx}otRwzE3yc;) literal 0 HcmV?d00001 diff --git a/examples/index.php b/examples/index.php index cf8647f..b87178f 100755 --- a/examples/index.php +++ b/examples/index.php @@ -71,6 +71,7 @@ ThirdParty::YAHOO => array('profile' => 'https://yahoo.com/%s', 'accounts' => array()), ThirdParty::GITHUB => array('profile' => 'https://github.com/%s', 'accounts' => array()), ThirdParty::AMAZON => array('profile' => 'https://amazon.com/%s', 'accounts' => array()), + ThirdParty::SPOTIFY => array('profile' => 'https://open.spotify.com/user/%s', 'accounts' => array()), ); $vendorAccounts = $storageRepository->getByAppUser( new ExistingAppUser($loggedInUser['id'], $loggedInUser['email']) @@ -115,7 +116,7 @@ ?> -
    +

    Logout

    @@ -129,6 +130,7 @@ +
    diff --git a/examples/sso.php b/examples/sso.php index 8f29a82..0daab89 100644 --- a/examples/sso.php +++ b/examples/sso.php @@ -25,6 +25,7 @@ use TSK\SSO\ThirdParty\GitHub\GitHubConnectionFactory; use TSK\SSO\ThirdParty\Google\GoogleConnectionFactory; use TSK\SSO\ThirdParty\Twitter\TwitterConnectionFactory; +use TSK\SSO\ThirdParty\Spotify\SpotifyConnectionFactory; use TSK\SSO\ThirdParty\VendorConnectionRevoker; use TSK\SSO\ThirdParty\Yahoo\YahooConnectionFactory; @@ -89,6 +90,15 @@ sprintf(CALLBACK_URL, ThirdParty::AMAZON) ) ); +$spotifyConnectionFactory = new SpotifyConnectionFactory(); +$connectionFactoryCollection->add( + ThirdParty::SPOTIFY, + $spotifyConnectionFactory->get( + '40de952ae63b403f894e47a6314bc64d', // demo real-app client id + '38eac415d0eb473bb0b4d1d732bc25d3', // demo real-app client secret + sprintf(CALLBACK_URL, ThirdParty::SPOTIFY) + ) +); try { $thirdPartyConnection = $connectionFactoryCollection->getByVendor($vendorName); diff --git a/src/ThirdParty.php b/src/ThirdParty.php index 4ccfbf5..1cba40f 100755 --- a/src/ThirdParty.php +++ b/src/ThirdParty.php @@ -19,6 +19,7 @@ class ThirdParty const FACEBOOK = 'facebook'; const LINKEDIN = 'linkedin'; const SLACK = 'slack'; + const SPOTIFY = 'spotify'; const TWITTER = 'twitter'; const YAHOO = 'yahoo'; } diff --git a/src/ThirdParty/Spotify/SpotifyApiConfiguration.php b/src/ThirdParty/Spotify/SpotifyApiConfiguration.php new file mode 100644 index 0000000..9fc00c4 --- /dev/null +++ b/src/ThirdParty/Spotify/SpotifyApiConfiguration.php @@ -0,0 +1,75 @@ + + * @date 25-02-2019 + */ + +namespace TSK\SSO\ThirdParty\Spotify; + +/** + * @package TSK\SSO\ThirdParty\Spotify + */ +class SpotifyApiConfiguration +{ + /** + * @var string + */ + private $clientId; + + /** + * @var string + */ + private $clientSecret; + + /** + * @var string + */ + private $redirectUrl; + + /** + * SpotifyApiConfiguration constructor. + * @param string $clientId + * @param string $clientSecret + * @param string $redirectUrl + */ + public function __construct($clientId, $clientSecret, $redirectUrl) + { + $this->clientId = $clientId; + $this->clientSecret = $clientSecret; + $this->redirectUrl = $redirectUrl; + } + + /** + * @return string + */ + public function clientId() + { + return $this->clientId; + } + + /** + * @return string + */ + public function clientSecret() + { + return $this->clientSecret; + } + + /** + * @return string + */ + public function redirectUrl() + { + return $this->redirectUrl; + } + + /** + * This is just to identify that, we initiated the login sequence (not someone else) + * + * @return string + */ + public function ourSecretState() + { + return 'dfeb6ef625880832f61c6f4bd737e11b'; + } +} diff --git a/src/ThirdParty/Spotify/SpotifyConnection.php b/src/ThirdParty/Spotify/SpotifyConnection.php new file mode 100644 index 0000000..73bb9e8 --- /dev/null +++ b/src/ThirdParty/Spotify/SpotifyConnection.php @@ -0,0 +1,142 @@ + + * @date 25-02-2019 + */ + +namespace TSK\SSO\ThirdParty\Spotify; + +use TSK\SSO\Http\CurlRequest; +use TSK\SSO\ThirdParty; +use TSK\SSO\ThirdParty\CommonAccessToken; +use TSK\SSO\ThirdParty\Exception\NoThirdPartyEmailFoundException; +use TSK\SSO\ThirdParty\Exception\ThirdPartyConnectionFailedException; +use TSK\SSO\ThirdParty\ThirdPartyUser; +use TSK\SSO\ThirdParty\VendorConnection; + +/** + * @codeCoverageIgnore + * @package TSK\SSO\ThirdParty\Spotify + * @see https://developer.spotify.com/documentation/general/guides/authorization-guide/ + */ +class SpotifyConnection implements VendorConnection +{ + const API_BASE = 'https://accounts.spotify.com'; + + /** + * @var SpotifyApiConfiguration + */ + private $apiConfiguration; + + /** + * @var CurlRequest + */ + private $curlClient; + + /** + * SpotifyConnection constructor. + * @param SpotifyApiConfiguration $apiConfiguration + * @param CurlRequest $curlClient + */ + public function __construct(SpotifyApiConfiguration $apiConfiguration, CurlRequest $curlClient) + { + $this->apiConfiguration = $apiConfiguration; + $this->curlClient = $curlClient; + } + + /** + * Use this to get a link to redirect a user to the third party login + * + * @return string|null + */ + public function getGrantUrl() + { + return sprintf( + '%s/authorize?client_id=%s&scope=%s&response_type=code&redirect_uri=%s&state=%s', + self::API_BASE, + $this->apiConfiguration->clientId(), + urlencode('user-read-email'), + urlencode($this->apiConfiguration->redirectUrl()), + $this->apiConfiguration->ourSecretState() + ); + } + + /** + * Grants a new access token + * + * @return CommonAccessToken + * @throws ThirdPartyConnectionFailedException + */ + public function grantNewAccessToken() + { + if (empty($_GET['code']) + || empty($_GET['state']) + || $_GET['state'] !== $this->apiConfiguration->ourSecretState() + ) { + throw new ThirdPartyConnectionFailedException('Invalid request!'); + } + + $accessTokenJsonResponse = $this->curlClient->postUrlEncoded( + sprintf("%s/api/token", self::API_BASE), + sprintf( + 'client_id=%s&client_secret=%s&grant_type=authorization_code&redirect_uri=%s&code=%s', + $this->apiConfiguration->clientId(), + $this->apiConfiguration->clientSecret(), + urlencode($this->apiConfiguration->redirectUrl()), + $_GET['code'] + ) + ); + + $accessTokenData = json_decode($accessTokenJsonResponse, true); + if (empty($accessTokenData['access_token'])) { + throw new ThirdPartyConnectionFailedException('Failed to establish a new third party vendor connection.'); + } + + return new CommonAccessToken( + $accessTokenData['access_token'], + ThirdParty::SPOTIFY + ); + } + + /** + * Use this to retrieve the current user's third party user data using there existing granted access token + * + * @param CommonAccessToken $accessToken + * @return ThirdPartyUser + * @throws NoThirdPartyEmailFoundException + * @throws ThirdPartyConnectionFailedException + */ + public function getSelf(CommonAccessToken $accessToken) + { + $userJsonInfo = $this->curlClient->get( + 'https://api.spotify.com/v1/me', + array( + sprintf('Authorization: Bearer %s', $accessToken->token()), + ) + ); + + $userInfo = json_decode($userJsonInfo, true); + if (empty($userInfo['email'])) { + throw new NoThirdPartyEmailFoundException('An email address cannot be found from vendor'); + } + + return new ThirdPartyUser( + $userInfo['id'], + $userInfo['display_name'], + $userInfo['email'] + ); + } + + /** + * Use this to revoke the access to the third party data. + * This will completely remove the access from the vendor side. + * + * @param CommonAccessToken $accessToken + * @return bool + */ + public function revokeAccess(CommonAccessToken $accessToken) + { + // noop : cannot find documentation on how to revoke the app's access. + return true; + } +} diff --git a/src/ThirdParty/Spotify/SpotifyConnectionFactory.php b/src/ThirdParty/Spotify/SpotifyConnectionFactory.php new file mode 100644 index 0000000..4c1ce90 --- /dev/null +++ b/src/ThirdParty/Spotify/SpotifyConnectionFactory.php @@ -0,0 +1,38 @@ + + * @date 25-02-2019 + */ + +namespace TSK\SSO\ThirdParty\Spotify; + +use TSK\SSO\Http\CurlRequest; +use TSK\SSO\ThirdParty\VendorConnection; +use TSK\SSO\ThirdParty\VendorConnectionFactory; + +/** + * @package TSK\SSO\ThirdParty\Spotify + */ +class SpotifyConnectionFactory implements VendorConnectionFactory +{ + /** + * Returns a Spotify Connection instance using given credentials. Visit the following link for credentials. + * @see https://developer.spotify.com/dashboard/applications + * + * @param string $clientId the client id which can be generated at the third party portal + * @param string $clientSecret this can be found similar to the clientId + * @param string $callbackUrl the url to callback after a third party auth attempt + * @return VendorConnection + */ + public function get($clientId, $clientSecret, $callbackUrl) + { + return new SpotifyConnection( + new SpotifyApiConfiguration( + $clientId, + $clientSecret, + $callbackUrl + ), + new CurlRequest() + ); + } +} diff --git a/tests/ThirdParty/Spotify/SpotifyApiConfigurationTest.php b/tests/ThirdParty/Spotify/SpotifyApiConfigurationTest.php new file mode 100644 index 0000000..be1db6d --- /dev/null +++ b/tests/ThirdParty/Spotify/SpotifyApiConfigurationTest.php @@ -0,0 +1,35 @@ + + * @date 25-02-2019 + */ + +namespace TSK\SSO\ThirdParty\Spotify; + +use PHPUnit\Framework\TestCase; + +class SpotifyApiConfigurationTest extends TestCase +{ + /** + * @test + */ + public function shouldReturnWithInstantiatedValues() + { + $apiConfig = array( + 'clientId' => 'client-id', + 'clientSecret' => 'client-secret', + 'redirectUrl' => 'http://www.tsk-webdevelopment.com', + ); + + $sut = new SpotifyApiConfiguration( + $apiConfig['clientId'], + $apiConfig['clientSecret'], + $apiConfig['redirectUrl'] + ); + + $this->assertSame($apiConfig['clientId'], $sut->clientId()); + $this->assertSame($apiConfig['clientSecret'], $sut->clientSecret()); + $this->assertSame($apiConfig['redirectUrl'], $sut->redirectUrl()); + $this->assertSame('dfeb6ef625880832f61c6f4bd737e11b', $sut->ourSecretState()); + } +} diff --git a/tests/ThirdParty/Spotify/SpotifyConnectionFactoryTest.php b/tests/ThirdParty/Spotify/SpotifyConnectionFactoryTest.php new file mode 100644 index 0000000..d9486ca --- /dev/null +++ b/tests/ThirdParty/Spotify/SpotifyConnectionFactoryTest.php @@ -0,0 +1,24 @@ + + * @date 25-02-2019 + */ + +namespace TSK\SSO\ThirdParty\Spotify; + +use PHPUnit\Framework\TestCase; + +class SpotifyConnectionFactoryTest extends TestCase +{ + /** + * @test + */ + public function shouldReturnAnInstanceOfASpotifyConnection() + { + $sut = new SpotifyConnectionFactory(); + + $actualConnection = $sut->get('clientId', 'clientSecret', 'www.tsk-webdevelopment.com'); + + $this->assertInstanceOf('\TSK\SSO\ThirdParty\Spotify\SpotifyConnection', $actualConnection); + } +} From a772d5f1de00fb64b185b48cb76856e9b4c6669f Mon Sep 17 00:00:00 2001 From: Tharanga Kothalawala Date: Wed, 28 Oct 2020 21:54:38 +0000 Subject: [PATCH 2/5] Adding zoom support (#6) --- README.md | 1 + examples/images/zoom.png | Bin 0 -> 9024 bytes examples/index.php | 2 +- examples/sso.php | 1 - src/ThirdParty.php | 1 + src/ThirdParty/Zoom/ZoomApiConfiguration.php | 77 ++++++++ src/ThirdParty/Zoom/ZoomConnection.php | 165 ++++++++++++++++++ src/ThirdParty/Zoom/ZoomConnectionFactory.php | 36 ++++ .../Zoom/ZoomApiConfigurationTest.php | 35 ++++ .../Zoom/ZoomConnectionFactoryTest.php | 24 +++ 10 files changed, 340 insertions(+), 2 deletions(-) create mode 100644 examples/images/zoom.png create mode 100755 src/ThirdParty/Zoom/ZoomApiConfiguration.php create mode 100755 src/ThirdParty/Zoom/ZoomConnection.php create mode 100755 src/ThirdParty/Zoom/ZoomConnectionFactory.php create mode 100644 tests/ThirdParty/Zoom/ZoomApiConfigurationTest.php create mode 100644 tests/ThirdParty/Zoom/ZoomConnectionFactoryTest.php diff --git a/README.md b/README.md index 78d2855..e03d958 100755 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ This is a library which can provision new accounts and can authenticate users ut * Spotify * Twitter * Yahoo +* Zoom # Structure diff --git a/examples/images/zoom.png b/examples/images/zoom.png new file mode 100644 index 0000000000000000000000000000000000000000..c5d6f985f3bc2566ddaa318eaccb1a3db3e82ce1 GIT binary patch literal 9024 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
    -

    Logout

    +

    Logout

    Sign in

    diff --git a/examples/sso.php b/examples/sso.php index 0daab89..9a8fc00 100644 --- a/examples/sso.php +++ b/examples/sso.php @@ -17,7 +17,6 @@ use TSK\SSO\Storage\FileSystemThirdPartyStorageRepository; use TSK\SSO\ThirdParty; use TSK\SSO\ThirdParty\Amazon\AmazonConnectionFactory; -use TSK\SSO\ThirdParty\CommonAccessToken; use TSK\SSO\ThirdParty\ThirdPartyConnectionCollection; use TSK\SSO\ThirdParty\Exception\NoThirdPartyEmailFoundException; use TSK\SSO\ThirdParty\Exception\ThirdPartyConnectionFailedException; diff --git a/src/ThirdParty.php b/src/ThirdParty.php index 1cba40f..9f92f73 100755 --- a/src/ThirdParty.php +++ b/src/ThirdParty.php @@ -22,4 +22,5 @@ class ThirdParty const SPOTIFY = 'spotify'; const TWITTER = 'twitter'; const YAHOO = 'yahoo'; + const ZOOM = 'zoom'; } diff --git a/src/ThirdParty/Zoom/ZoomApiConfiguration.php b/src/ThirdParty/Zoom/ZoomApiConfiguration.php new file mode 100755 index 0000000..9bbd060 --- /dev/null +++ b/src/ThirdParty/Zoom/ZoomApiConfiguration.php @@ -0,0 +1,77 @@ + + * @date 28-10-2020 + */ + +namespace TSK\SSO\ThirdParty\Zoom; + +/** + * @package TSK\SSO\ThirdParty\Zoom + * @see https://api.Zoom.com/apps + */ +class ZoomApiConfiguration +{ + /** + * @var string + */ + private $clientId; + + /** + * @var string + */ + private $clientSecret; + + /** + * @var string + */ + private $redirectUrl; + + /** + * ZoomApiConfiguration constructor. + * + * @param string $clientId + * @param string $clientSecret + * @param string $redirectUrl + */ + public function __construct($clientId, $clientSecret, $redirectUrl) + { + $this->clientId = $clientId; + $this->clientSecret = $clientSecret; + $this->redirectUrl = $redirectUrl; + } + + /** + * @return string + */ + public function clientId() + { + return $this->clientId; + } + + /** + * @return string + */ + public function clientSecret() + { + return $this->clientSecret; + } + + /** + * @return string + */ + public function redirectUrl() + { + return $this->redirectUrl; + } + + /** + * This is just to identify that, we initiated the login sequence (not someone else) + * + * @return string + */ + public function ourSecretState() + { + return 'dfeb6ef625880832f61c6f4bd737e11b'; + } +} diff --git a/src/ThirdParty/Zoom/ZoomConnection.php b/src/ThirdParty/Zoom/ZoomConnection.php new file mode 100755 index 0000000..05a0b4b --- /dev/null +++ b/src/ThirdParty/Zoom/ZoomConnection.php @@ -0,0 +1,165 @@ + + * @date 28-10-2020 + */ + +namespace TSK\SSO\ThirdParty\Zoom; + +use TSK\SSO\Http\CurlRequest; +use TSK\SSO\ThirdParty; +use TSK\SSO\ThirdParty\CommonAccessToken; +use TSK\SSO\ThirdParty\Exception\NoThirdPartyEmailFoundException; +use TSK\SSO\ThirdParty\Exception\ThirdPartyConnectionFailedException; +use TSK\SSO\ThirdParty\ThirdPartyUser; +use TSK\SSO\ThirdParty\VendorConnection; + +/** + * @codeCoverageIgnore + * @package TSK\SSO\ThirdParty\Zoom + * @see https://marketplace.zoom.us/docs/guides/auth/oauth + */ +class ZoomConnection implements VendorConnection +{ + const API_BASE = 'https://zoom.us'; + + /** + * @var ZoomApiConfiguration + */ + private $apiConfiguration; + + /** + * @var CurlRequest + */ + private $curlClient; + + /** + * @param ZoomApiConfiguration $apiConfiguration + * @param CurlRequest $curlClient + */ + public function __construct(ZoomApiConfiguration $apiConfiguration, CurlRequest $curlClient) + { + $this->apiConfiguration = $apiConfiguration; + $this->curlClient = $curlClient; + } + + /** + * Use this to redirect the user to the third party login page to grant permission to use their account. + */ + public function getGrantUrl() + { + return sprintf( + '%s/oauth/authorize?response_type=code&client_id=%s&redirect_uri=%s&state=%s', + self::API_BASE, + $this->apiConfiguration->clientId(), + urldecode($this->apiConfiguration->redirectUrl()), + $this->apiConfiguration->ourSecretState() + ); + } + + /** + * Grants a new access token + * + * @return CommonAccessToken + * @throws ThirdPartyConnectionFailedException + */ + public function grantNewAccessToken() + { + if (empty($_GET['code']) + || empty($_GET['state']) + || $_GET['state'] !== $this->apiConfiguration->ourSecretState() + ) { + throw new ThirdPartyConnectionFailedException('Invalid request!'); + } + + $accessTokenJsonInfo = $this->curlClient->postUrlEncoded( + sprintf("%s/oauth/token", self::API_BASE), + sprintf( + 'client_id=%s&client_secret=%s&grant_type=authorization_code&redirect_uri=%s&code=%s', + $this->apiConfiguration->clientId(), + $this->apiConfiguration->clientSecret(), + urlencode($this->apiConfiguration->redirectUrl()), + $_GET['code'] + ), + array( + 'Authorization' => sprintf( + 'Basic %s', + base64_encode( + sprintf('%s:%s', $this->apiConfiguration->clientId(), $this->apiConfiguration->clientSecret()) + ) + ), + ) + ); + + $accessTokenInfo = json_decode($accessTokenJsonInfo, true); + if (empty($accessTokenInfo['access_token'])) { + throw new ThirdPartyConnectionFailedException( + 'An error occurred while getting the access.' + ); + } + + return new CommonAccessToken($accessTokenInfo['access_token'], ThirdParty::ZOOM); + } + + /** + * Use this to retrieve the current user's third party user data using there existing granted access token + * + * @param CommonAccessToken $accessToken + * + * @return ThirdPartyUser + * @throws NoThirdPartyEmailFoundException + * @throws ThirdPartyConnectionFailedException + */ + public function getSelf(CommonAccessToken $accessToken) + { + $userJsonInfo = $this->curlClient->get( + 'https://api.zoom.us/v2/users/me', + array( + sprintf('Authorization: Bearer %s', $accessToken->token()), + ) + ); + + $userInfo = json_decode($userJsonInfo, true); + if (empty($userInfo['email'])) { + throw new NoThirdPartyEmailFoundException('An email address cannot be found from vendor'); + } + + return new ThirdPartyUser( + $userInfo['id'], + sprintf('%s %s', $userInfo['first_name'], $userInfo['last_name']), + $userInfo['email'], + $userInfo['pic_url'] + ); + } + + /** + * Use this to revoke the access to the third party data. + * This will completely remove the access from the vendor side. + * + * @param CommonAccessToken $accessToken + * + * @return bool + */ + public function revokeAccess(CommonAccessToken $accessToken) + { + $revokeJsonInfo = $this->curlClient->post( + sprintf( + "%s/oauth/revoke?token=%s", + self::API_BASE, + $accessToken->token() + ), + array(), + array( + 'Authorization' => sprintf( + 'Basic %s', + base64_encode( + sprintf('%s:%s', $this->apiConfiguration->clientId(), $this->apiConfiguration->clientSecret()) + ) + ), + ) + ); + + $revokeInfo = json_decode($revokeJsonInfo, true); + return (!empty($revokeInfo['status']) && $revokeInfo['status'] === 'success'); + } +} diff --git a/src/ThirdParty/Zoom/ZoomConnectionFactory.php b/src/ThirdParty/Zoom/ZoomConnectionFactory.php new file mode 100755 index 0000000..604c622 --- /dev/null +++ b/src/ThirdParty/Zoom/ZoomConnectionFactory.php @@ -0,0 +1,36 @@ + + * @date 28-10-2020 + */ + +namespace TSK\SSO\ThirdParty\Zoom; + +use TSK\SSO\ThirdParty\VendorConnection; +use TSK\SSO\ThirdParty\VendorConnectionFactory; +use TSK\SSO\Http\CurlRequest; + +/** + * @package TSK\SSO\ThirdParty\Zoom + */ +class ZoomConnectionFactory implements VendorConnectionFactory +{ + /** + * @param string $clientId the client id which can be generated at the third party portal + * @param string $clientSecret this can be found similar to the clientId + * @param string $callbackUrl the url to callback after a third party auth attempt + * + * @return VendorConnection + */ + public function get($clientId, $clientSecret, $callbackUrl) + { + return new ZoomConnection( + new ZoomApiConfiguration( + $clientId, + $clientSecret, + $callbackUrl + ), + new CurlRequest() + ); + } +} diff --git a/tests/ThirdParty/Zoom/ZoomApiConfigurationTest.php b/tests/ThirdParty/Zoom/ZoomApiConfigurationTest.php new file mode 100644 index 0000000..ca58089 --- /dev/null +++ b/tests/ThirdParty/Zoom/ZoomApiConfigurationTest.php @@ -0,0 +1,35 @@ + + * @date 28-10-2020 + */ + +namespace TSK\SSO\ThirdParty\Zoom; + +use PHPUnit\Framework\TestCase; + +class ZoomApiConfigurationTest extends TestCase +{ + /** + * @test + */ + public function shouldReturnWithInstantiatedValues() + { + $apiConfig = array( + 'clientId' => 'client-id', + 'clientSecret' => 'client-secret', + 'redirectUrl' => 'http://www.tsk-webdevelopment.com', + ); + + $sut = new ZoomApiConfiguration( + $apiConfig['clientId'], + $apiConfig['clientSecret'], + $apiConfig['redirectUrl'] + ); + + $this->assertSame($apiConfig['clientId'], $sut->clientId()); + $this->assertSame($apiConfig['clientSecret'], $sut->clientSecret()); + $this->assertSame($apiConfig['redirectUrl'], $sut->redirectUrl()); + $this->assertSame('dfeb6ef625880832f61c6f4bd737e11b', $sut->ourSecretState()); + } +} diff --git a/tests/ThirdParty/Zoom/ZoomConnectionFactoryTest.php b/tests/ThirdParty/Zoom/ZoomConnectionFactoryTest.php new file mode 100644 index 0000000..e4830c6 --- /dev/null +++ b/tests/ThirdParty/Zoom/ZoomConnectionFactoryTest.php @@ -0,0 +1,24 @@ + + * @date 28-10-2020 + */ + +namespace TSK\SSO\ThirdParty\Zoom; + +use PHPUnit\Framework\TestCase; + +class ZoomConnectionFactoryTest extends TestCase +{ + /** + * @test + */ + public function shouldReturnAnInstanceOfAZoomConnection() + { + $sut = new ZoomConnectionFactory(); + + $actualConnection = $sut->get('clientId', 'clientSecret', 'www.tsk-webdevelopment.com'); + + $this->assertInstanceOf('\TSK\SSO\ThirdParty\Zoom\ZoomConnection', $actualConnection); + } +} From a1c5a5ba69c8b4557a5576e1a531c272f067359d Mon Sep 17 00:00:00 2001 From: Tharanga Kothalawala Date: Wed, 16 Dec 2020 20:56:37 +0000 Subject: [PATCH 3/5] storing the refresh token in the mapping table (#7) --- sql/db-template.sql | 3 +- .../FileSystemThirdPartyStorageRepository.php | 1 + .../MysqliThirdPartyStorageRepository.php | 7 +++- .../PdoThirdPartyStorageRepository.php | 5 +++ ...PeclMongoDbThirdPartyStorageRepository.php | 2 + src/ThirdParty/CommonAccessToken.php | 25 ++++++++++++ src/ThirdParty/Zoom/ZoomConnection.php | 39 ++++++++++++++----- ...eSystemThirdPartyStorageRepositoryTest.php | 2 +- 8 files changed, 71 insertions(+), 13 deletions(-) diff --git a/sql/db-template.sql b/sql/db-template.sql index 3d537e9..b76d8ad 100755 --- a/sql/db-template.sql +++ b/sql/db-template.sql @@ -4,9 +4,10 @@ CREATE TABLE `thirdparty_connections` ( `id` INT(10) NOT NULL AUTO_INCREMENT, `app_user_id` VARCHAR(255) NOT NULL COMMENT 'unique user id belongs to the application logic', - `vendor_name` ENUM('facebook', 'instagram', 'twitter', 'google', 'slack', 'linkedin', 'yahoo') NOT NULL, + `vendor_name` ENUM('amazon', 'facebook', 'github', 'google', 'linkedin', 'slack', 'spotify', 'twitter', 'yahoo', 'zoom') NOT NULL, `vendor_email` VARCHAR(255) NOT NULL COMMENT 'email address associated with the third party account', `vendor_access_token` TEXT, + `vendor_refresh_token` TEXT, `vendor_data` TEXT COMMENT 'any other data related to the vendor user in JSON format', `created_at` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00', `updated_at` TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP, diff --git a/src/Storage/FileSystemThirdPartyStorageRepository.php b/src/Storage/FileSystemThirdPartyStorageRepository.php index 18dcfdf..16d3bdd 100755 --- a/src/Storage/FileSystemThirdPartyStorageRepository.php +++ b/src/Storage/FileSystemThirdPartyStorageRepository.php @@ -122,6 +122,7 @@ public function save( 'vendor_name' => $accessToken->vendor(), 'vendor_email' => $thirdPartyUser->email(), 'vendor_access_token' => $accessToken->token(), + 'vendor_refresh_token' => $accessToken->getRefreshToken(), 'vendor_data' => json_encode($thirdPartyUser->toArray()), 'created_at' => date('Y-m-d H:i:00'), ); diff --git a/src/Storage/MysqliThirdPartyStorageRepository.php b/src/Storage/MysqliThirdPartyStorageRepository.php index 97b2aaf..4eae327 100755 --- a/src/Storage/MysqliThirdPartyStorageRepository.php +++ b/src/Storage/MysqliThirdPartyStorageRepository.php @@ -143,15 +143,17 @@ public function save( `vendor_name`, `vendor_email`, `vendor_access_token`, + `vendor_refresh_token`, `vendor_data`, `created_at` ) VALUES ( - ?, ?, ?, ?, ?, NOW() + ?, ?, ?, ?, ?, ?, NOW() ) ON DUPLICATE KEY UPDATE `vendor_access_token` = VALUES(`vendor_access_token`), + `vendor_refresh_token` = VALUES(`vendor_refresh_token`), `vendor_data` = VALUES(`vendor_data`), `updated_at` = NOW() SQL; @@ -161,9 +163,10 @@ public function save( $vendorName = $accessToken->vendor(); $vendorEmail = $thirdPartyUser->email(); $vendorToken = $accessToken->token(); + $vendorRefreshToken = $accessToken->getRefreshToken(); $stmt = $this->dbConnection->prepare($sql); - $stmt->bind_param('sssss', $appUserId, $vendorName, $vendorEmail, $vendorToken, $vendorData); + $stmt->bind_param('ssssss', $appUserId, $vendorName, $vendorEmail, $vendorToken, $vendorRefreshToken, $vendorData); $saved = $stmt->execute(); if (!$saved) { throw new DataCannotBeStoredException( diff --git a/src/Storage/PdoThirdPartyStorageRepository.php b/src/Storage/PdoThirdPartyStorageRepository.php index 058b52f..6057ddb 100755 --- a/src/Storage/PdoThirdPartyStorageRepository.php +++ b/src/Storage/PdoThirdPartyStorageRepository.php @@ -121,6 +121,7 @@ public function save( `vendor_name`, `vendor_email`, `vendor_access_token`, + `vendor_refresh_token`, `vendor_data`, `created_at` ) @@ -130,11 +131,13 @@ public function save( :vendorName, :vendorEmail, :vendorToken, + :vendorRefreshToken, :vendorData, NOW() ) ON DUPLICATE KEY UPDATE `vendor_access_token` = VALUES(`vendor_access_token`), + `vendor_refresh_token` = VALUES(`vendor_refresh_token`), `vendor_data` = VALUES(`vendor_data`), `updated_at` = NOW() SQL; @@ -144,12 +147,14 @@ public function save( $vendorName = $accessToken->vendor(); $vendorEmail = $thirdPartyUser->email(); $vendorToken = $accessToken->token(); + $vendorRefreshToken = $accessToken->getRefreshToken(); $stmt = $this->dbConnection->prepare($sql); $stmt->bindParam(':appUserId', $appUserId, PDO::PARAM_STR); $stmt->bindParam(':vendorName', $vendorName, PDO::PARAM_STR); $stmt->bindParam(':vendorEmail', $vendorEmail, PDO::PARAM_STR); $stmt->bindParam(':vendorToken', $vendorToken, PDO::PARAM_STR); + $stmt->bindParam(':vendorRefreshToken', $vendorRefreshToken, PDO::PARAM_STR); $stmt->bindParam(':vendorData', $vendorData, PDO::PARAM_STR); $stmt->execute(); } catch (PDOException $ex) { diff --git a/src/Storage/PeclMongoDbThirdPartyStorageRepository.php b/src/Storage/PeclMongoDbThirdPartyStorageRepository.php index 9a883d7..c1e7a23 100755 --- a/src/Storage/PeclMongoDbThirdPartyStorageRepository.php +++ b/src/Storage/PeclMongoDbThirdPartyStorageRepository.php @@ -149,6 +149,7 @@ public function save( array( '$set' => array( 'vendor_access_token' => $accessToken->token(), + 'vendor_refresh_token' => $accessToken->getRefreshToken(), 'vendor_data' => json_encode($thirdPartyUser->toArray()), ), ) @@ -160,6 +161,7 @@ public function save( 'vendor_name' => $accessToken->vendor(), 'vendor_email' => $thirdPartyUser->email(), 'vendor_access_token' => $accessToken->token(), + 'vendor_refresh_token' => $accessToken->getRefreshToken(), 'vendor_data' => json_encode($thirdPartyUser->toArray()), 'created_at' => date('Y-m-d H:i:s'), )); diff --git a/src/ThirdParty/CommonAccessToken.php b/src/ThirdParty/CommonAccessToken.php index d192750..66982df 100755 --- a/src/ThirdParty/CommonAccessToken.php +++ b/src/ThirdParty/CommonAccessToken.php @@ -26,6 +26,11 @@ class CommonAccessToken */ private $email; + /** + * @var string token used to refresh this access token + */ + private $refreshToken; + /** * This value object can be used to represent an access token by any third party vendor. * @@ -69,4 +74,24 @@ public function email() { return $this->email; } + + /** + * returns the token used to refresh this access token + * + * @return string + */ + public function getRefreshToken() + { + return $this->refreshToken; + } + + /** + * Set the token used to refresh this access token + * + * @param string $refreshToken refresh token + */ + public function setRefreshToken($refreshToken) + { + $this->refreshToken = $refreshToken; + } } diff --git a/src/ThirdParty/Zoom/ZoomConnection.php b/src/ThirdParty/Zoom/ZoomConnection.php index 05a0b4b..63e41e3 100755 --- a/src/ThirdParty/Zoom/ZoomConnection.php +++ b/src/ThirdParty/Zoom/ZoomConnection.php @@ -72,6 +72,15 @@ public function grantNewAccessToken() throw new ThirdPartyConnectionFailedException('Invalid request!'); } + $headers = array( + 'Authorization' => sprintf( + 'Basic %s', + base64_encode( + sprintf('%s:%s', $this->apiConfiguration->clientId(), $this->apiConfiguration->clientSecret()) + ) + ), + ); + $accessTokenJsonInfo = $this->curlClient->postUrlEncoded( sprintf("%s/oauth/token", self::API_BASE), sprintf( @@ -81,14 +90,7 @@ public function grantNewAccessToken() urlencode($this->apiConfiguration->redirectUrl()), $_GET['code'] ), - array( - 'Authorization' => sprintf( - 'Basic %s', - base64_encode( - sprintf('%s:%s', $this->apiConfiguration->clientId(), $this->apiConfiguration->clientSecret()) - ) - ), - ) + $headers ); $accessTokenInfo = json_decode($accessTokenJsonInfo, true); @@ -98,7 +100,26 @@ public function grantNewAccessToken() ); } - return new CommonAccessToken($accessTokenInfo['access_token'], ThirdParty::ZOOM); + $accessToken = new CommonAccessToken($accessTokenInfo['access_token'], ThirdParty::ZOOM); + if (!empty($accessTokenInfo['refresh_token'])) { + $accessToken->setRefreshToken($accessTokenInfo['refresh_token']); + } + + /* + $refreshTokenJsonInfo = $this->curlClient->post( + sprintf("%s/oauth/token?grant_type=refresh_token&refresh_token=%s", self::API_BASE, $accessTokenInfo['refresh_token']), + array(), + $headers + ); + + // if refresh token found, return that or return the access token + $refreshTokenInfo = json_decode($refreshTokenJsonInfo, true); + if (!empty($refreshTokenInfo['access_token']) && !empty($refreshTokenInfo['refresh_token'])) { + $accessToken = new CommonAccessToken($refreshTokenInfo['access_token'], ThirdParty::ZOOM); + $accessToken->setRefreshToken($refreshTokenInfo['refresh_token']); + }//*/ + + return $accessToken; } /** diff --git a/tests/Storage/FileSystemThirdPartyStorageRepositoryTest.php b/tests/Storage/FileSystemThirdPartyStorageRepositoryTest.php index a90c73d..3ad5d69 100644 --- a/tests/Storage/FileSystemThirdPartyStorageRepositoryTest.php +++ b/tests/Storage/FileSystemThirdPartyStorageRepositoryTest.php @@ -35,7 +35,7 @@ public function shouldStoreNewUserDataInTheThirdPartyStore() $actualContents = file_get_contents(__DIR__ . '/' . FileSystemThirdPartyStorageRepository::FILE_NAME); $this->assertSame( - sprintf('{"vendor::vendor-email@test.com":{"app_user_id":"id","vendor_name":"vendor","vendor_email":"vendor-email@test.com","vendor_access_token":"token","vendor_data":"{\"id\":\"id\",\"name\":\"name\",\"email\":\"vendor-email@test.com\",\"avatar\":\"\",\"gender\":\"\"}","created_at":"%s"}}', $now), + sprintf('{"vendor::vendor-email@test.com":{"app_user_id":"id","vendor_name":"vendor","vendor_email":"vendor-email@test.com","vendor_access_token":"token","vendor_refresh_token":null,"vendor_data":"{\"id\":\"id\",\"name\":\"name\",\"email\":\"vendor-email@test.com\",\"avatar\":\"\",\"gender\":\"\"}","created_at":"%s"}}', $now), $actualContents ); } From 5310527d1ddc602ae8f5eff090bf90cab847cffa Mon Sep 17 00:00:00 2001 From: Tharanga Kothalawala Date: Wed, 16 Dec 2020 20:57:26 +0000 Subject: [PATCH 4/5] tests updated --- tests/ThirdParty/CommonAccessTokenTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/ThirdParty/CommonAccessTokenTest.php b/tests/ThirdParty/CommonAccessTokenTest.php index 5df0bb0..7398fc9 100644 --- a/tests/ThirdParty/CommonAccessTokenTest.php +++ b/tests/ThirdParty/CommonAccessTokenTest.php @@ -17,6 +17,7 @@ public function shouldReturnWithInstantiatedValues() { $token = array( 'token' => 'the-token', + 'refresh_token' => 'the-refresh-token', 'vendor' => 'test-vendor', 'email' => 'vendor-email@test.com', ); @@ -26,9 +27,11 @@ public function shouldReturnWithInstantiatedValues() $token['vendor'], $token['email'] ); + $sut->setRefreshToken($token['refresh_token']); $this->assertSame($token['token'], $sut->token()); $this->assertSame($token['vendor'], $sut->vendor()); $this->assertSame($token['email'], $sut->email()); + $this->assertSame($token['refresh_token'], $sut->getRefreshToken()); } } From a5945ba89cfd47a31a8ea24552bf3beabe8038f2 Mon Sep 17 00:00:00 2001 From: Tharanga Kothalawala Date: Mon, 15 Feb 2021 23:04:23 +0000 Subject: [PATCH 5/5] Adding Stripe Payment Gateway SSO connection support (#8) * Adding Stripe Payment Gateway SSO connection support * updated composer and suggesting to use stripe for Stripe connection. also ran phpcbf --- README.md | 1 + composer.json | 22 ++- src/ThirdParty.php | 3 +- src/ThirdParty/Stripe/StripeConfiguration.php | 79 ++++++++++ src/ThirdParty/Stripe/StripeConnection.php | 140 ++++++++++++++++++ .../Stripe/StripeConnectionFactory.php | 38 +++++ tests/ThirdParty/CommonAccessTokenTest.php | 74 ++++----- .../Stripe/StripeConfigurationTest.php | 34 +++++ .../Stripe/StripeConnectionFactoryTest.php | 24 +++ .../ThirdPartyConnectionCollectionTest.php | 92 ++++++------ tests/ThirdParty/ThirdPartyUserTest.php | 80 +++++----- 11 files changed, 458 insertions(+), 129 deletions(-) create mode 100644 src/ThirdParty/Stripe/StripeConfiguration.php create mode 100644 src/ThirdParty/Stripe/StripeConnection.php create mode 100644 src/ThirdParty/Stripe/StripeConnectionFactory.php create mode 100644 tests/ThirdParty/Stripe/StripeConfigurationTest.php create mode 100644 tests/ThirdParty/Stripe/StripeConnectionFactoryTest.php diff --git a/README.md b/README.md index e03d958..adf55f7 100755 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ This is a library which can provision new accounts and can authenticate users ut * LinkedIn * Slack * Spotify +* Stripe * Twitter * Yahoo * Zoom diff --git a/composer.json b/composer.json index c5d7d56..b2360c1 100755 --- a/composer.json +++ b/composer.json @@ -1,13 +1,22 @@ { "name": "tharangakothalawala/sso", "description": "This is a library which can provision new accounts and to authenticate users utilizing third party vendor connections.", - "keywords": ["sso", "signup", "signin", "register", "login", "provision"], + "keywords": [ + "sso", + "signup", + "signin", + "register", + "login", + "provision" + ], "type": "library", "license": "MIT", - "authors": [{ - "name": "Tharanga Kothalawala", - "email": "tharanga.kothalawala@gmail.com" - }], + "authors": [ + { + "name": "Tharanga Kothalawala", + "email": "tharanga.kothalawala@gmail.com" + } + ], "minimum-stability": "dev", "require": { "php": ">=5.3", @@ -30,5 +39,8 @@ "psr-4": { "TSK\\SSO\\": "tests/" } + }, + "suggest": { + "stripe/stripe-php": "Allows using the Stripe Connection" } } diff --git a/src/ThirdParty.php b/src/ThirdParty.php index 9f92f73..5e85731 100755 --- a/src/ThirdParty.php +++ b/src/ThirdParty.php @@ -1,7 +1,7 @@ - * @date 30-12-2018 + * @date 30-12-2018 */ namespace TSK\SSO; @@ -20,6 +20,7 @@ class ThirdParty const LINKEDIN = 'linkedin'; const SLACK = 'slack'; const SPOTIFY = 'spotify'; + const STRIPE = 'stripe'; const TWITTER = 'twitter'; const YAHOO = 'yahoo'; const ZOOM = 'zoom'; diff --git a/src/ThirdParty/Stripe/StripeConfiguration.php b/src/ThirdParty/Stripe/StripeConfiguration.php new file mode 100644 index 0000000..7376469 --- /dev/null +++ b/src/ThirdParty/Stripe/StripeConfiguration.php @@ -0,0 +1,79 @@ + + * @date 15-02-2021 + */ + +namespace TSK\SSO\ThirdParty\Stripe; + +/** + * This represents a stripe oauth configuration + * + * @package TSK\SSO\ThirdParty\Stripe + */ +class StripeConfiguration +{ + /** + * @var string Stripe Client ID + * @see https://dashboard.stripe.com/settings/applications + */ + private $clientId; + + /** + * @var string Stripe Client Secret + */ + private $clientSecret; + + /** + * @var string Redirection URL back to the client application + */ + private $redirectUrl; + + /** + * StripeConfiguration constructor. + * + * @param string $clientId Stripe Client ID + * @param string $clientSecret Stripe Client Secret + * @param string $redirectUrl Redirection URL back to the client application + */ + public function __construct($clientId, $clientSecret, $redirectUrl) + { + $this->clientId = $clientId; + $this->clientSecret = $clientSecret; + $this->redirectUrl = $redirectUrl; + } + + /** + * @return string + */ + public function clientId() + { + return $this->clientId; + } + + /** + * @return string + */ + public function clientSecret() + { + return $this->clientSecret; + } + + /** + * @return string + */ + public function redirectUrl() + { + return $this->redirectUrl; + } + + /** + * This is just to identify that, we initiated the login sequence (not someone else) + * + * @return string + */ + public function ourSecretState() + { + return md5($this->clientId); + } +} diff --git a/src/ThirdParty/Stripe/StripeConnection.php b/src/ThirdParty/Stripe/StripeConnection.php new file mode 100644 index 0000000..426b8fd --- /dev/null +++ b/src/ThirdParty/Stripe/StripeConnection.php @@ -0,0 +1,140 @@ + + * @date 15-02-2021 + */ + +namespace TSK\SSO\ThirdParty\Stripe; + +use Stripe\Account; +use Stripe\Exception\ApiErrorException; +use Stripe\Exception\OAuth\OAuthErrorException; +use Stripe\OAuth; +use TSK\SSO\ThirdParty; +use TSK\SSO\ThirdParty\CommonAccessToken; +use TSK\SSO\ThirdParty\Exception\NoThirdPartyEmailFoundException; +use TSK\SSO\ThirdParty\Exception\ThirdPartyConnectionFailedException; +use TSK\SSO\ThirdParty\ThirdPartyUser; +use TSK\SSO\ThirdParty\VendorConnection; + +/** + * @codeCoverageIgnore + * @package TSK\SSO\ThirdParty\Stripe + * @see https://stripe.com/docs/connect/oauth-standard-accounts + */ +class StripeConnection implements VendorConnection +{ + /** + * @var StripeConfiguration + */ + private $configuration; + + /** + * StripeConnectConnection constructor. + * + * @param StripeConfiguration $configuration + */ + public function __construct(StripeConfiguration $configuration) + { + if (class_exists('\Stripe\Stripe')) { + \Stripe\Stripe::setApiKey($configuration->clientSecret()); + } + $this->configuration = $configuration; + } + + /** + * Use this to get a link to redirect a user to the third party login + * + * @return string|null + */ + public function getGrantUrl() + { + return sprintf( + "https://connect.stripe.com/oauth/authorize?response_type=code&client_id=%s&scope=read_write&state=%s&redirect_uri=%s", + $this->configuration->clientId(), + $this->configuration->ourSecretState(), + $this->configuration->redirectUrl() + ); + } + + /** + * Grants a new access token + * + * @return CommonAccessToken + * @throws ThirdPartyConnectionFailedException + */ + public function grantNewAccessToken() + { + if (empty($_GET['code']) + || empty($_GET['state']) + || $_GET['state'] !== $this->configuration->ourSecretState() + ) { + throw new ThirdPartyConnectionFailedException('Invalid request!'); + } + + try { + $response = OAuth::token([ + 'grant_type' => 'authorization_code', + 'code' => $_GET['code'], + ]); + } catch (OAuthErrorException $ex) { + throw new ThirdPartyConnectionFailedException($ex->getMessage(), $ex->getCode(), $ex); + } + + return new CommonAccessToken( + $response->access_token, + ThirdParty::STRIPE, + $response->stripe_user_id + ); + } + + /** + * Use this to retrieve the current user's third party user data using there existing granted access token + * + * @param CommonAccessToken $accessToken + * + * @return ThirdPartyUser + * @throws NoThirdPartyEmailFoundException + * @throws ThirdPartyConnectionFailedException + */ + public function getSelf(CommonAccessToken $accessToken) + { + try { + $account = Account::retrieve($accessToken->email()); + } catch (ApiErrorException $ex) { + throw new ThirdPartyConnectionFailedException($ex->getMessage(), $ex->getCode(), $ex); + } + + if (empty($account->id)) { + throw new NoThirdPartyEmailFoundException("Stripe account and/or email address cannot be retrieved"); + } + + return new ThirdPartyUser( + $account->id, + !empty($account->business_profile->name) ? $account->business_profile->name : '', + !empty($account->email) ? $account->email : '' + ); + } + + /** + * Use this to revoke the access to the third party data. + * This will completely remove the access from the vendor side. + * + * @param CommonAccessToken $accessToken + * + * @return bool + */ + public function revokeAccess(CommonAccessToken $accessToken) + { + try { + OAuth::deauthorize([ + 'client_id' => $this->configuration->clientId(), + 'stripe_user_id' => $accessToken->email(), + ]); + } catch (OAuthErrorException $ex) { + return false; + } + + return true; + } +} diff --git a/src/ThirdParty/Stripe/StripeConnectionFactory.php b/src/ThirdParty/Stripe/StripeConnectionFactory.php new file mode 100644 index 0000000..c2f36d9 --- /dev/null +++ b/src/ThirdParty/Stripe/StripeConnectionFactory.php @@ -0,0 +1,38 @@ + + * @date 15-02-2021 + */ + +namespace TSK\SSO\ThirdParty\Stripe; + +use TSK\SSO\ThirdParty\VendorConnection; +use TSK\SSO\ThirdParty\VendorConnectionFactory; + +/** + * @package TSK\SSO\ThirdParty\Spotify + */ +class StripeConnectionFactory implements VendorConnectionFactory +{ + /** + * Returns a Stripe Connection instance using given credentials. Visit the following links for credentials. + * @see https://dashboard.stripe.com/apikeys + * @see https://dashboard.stripe.com/settings/applications + * + * @param string $clientId the client id which can be generated at the Stripe portal + * @param string $clientSecret this can be found similar to the clientId + * @param string $callbackUrl the url to callback after a third party auth attempt + * + * @return VendorConnection + */ + public function get($clientId, $clientSecret, $callbackUrl) + { + return new StripeConnection( + new StripeConfiguration( + $clientId, + $clientSecret, + $callbackUrl + ) + ); + } +} diff --git a/tests/ThirdParty/CommonAccessTokenTest.php b/tests/ThirdParty/CommonAccessTokenTest.php index 7398fc9..c8251b5 100644 --- a/tests/ThirdParty/CommonAccessTokenTest.php +++ b/tests/ThirdParty/CommonAccessTokenTest.php @@ -1,37 +1,37 @@ - - * @date 01-01-2019 - */ - -namespace TSK\SSO\ThirdParty; - -use PHPUnit\Framework\TestCase; - -class CommonAccessTokenTest extends TestCase -{ - /** - * @test - */ - public function shouldReturnWithInstantiatedValues() - { - $token = array( - 'token' => 'the-token', - 'refresh_token' => 'the-refresh-token', - 'vendor' => 'test-vendor', - 'email' => 'vendor-email@test.com', - ); - - $sut = new CommonAccessToken( - $token['token'], - $token['vendor'], - $token['email'] - ); - $sut->setRefreshToken($token['refresh_token']); - - $this->assertSame($token['token'], $sut->token()); - $this->assertSame($token['vendor'], $sut->vendor()); - $this->assertSame($token['email'], $sut->email()); - $this->assertSame($token['refresh_token'], $sut->getRefreshToken()); - } -} + + * @date 01-01-2019 + */ + +namespace TSK\SSO\ThirdParty; + +use PHPUnit\Framework\TestCase; + +class CommonAccessTokenTest extends TestCase +{ + /** + * @test + */ + public function shouldReturnWithInstantiatedValues() + { + $token = array( + 'token' => 'the-token', + 'refresh_token' => 'the-refresh-token', + 'vendor' => 'test-vendor', + 'email' => 'vendor-email@test.com', + ); + + $sut = new CommonAccessToken( + $token['token'], + $token['vendor'], + $token['email'] + ); + $sut->setRefreshToken($token['refresh_token']); + + $this->assertSame($token['token'], $sut->token()); + $this->assertSame($token['vendor'], $sut->vendor()); + $this->assertSame($token['email'], $sut->email()); + $this->assertSame($token['refresh_token'], $sut->getRefreshToken()); + } +} diff --git a/tests/ThirdParty/Stripe/StripeConfigurationTest.php b/tests/ThirdParty/Stripe/StripeConfigurationTest.php new file mode 100644 index 0000000..6b2437f --- /dev/null +++ b/tests/ThirdParty/Stripe/StripeConfigurationTest.php @@ -0,0 +1,34 @@ + + * @date 15-02-2021 + */ + +namespace TSK\SSO\ThirdParty\Stripe; + +use PHPUnit\Framework\TestCase; + +class StripeConfigurationTest extends TestCase +{ + /** + * @test + */ + public function shouldReturnWithInstantiatedValues() + { + $apiConfig = array( + 'apiKey' => 'api-key', + 'apiSecret' => 'api-secret', + 'redirectUrl' => 'http://www.tsk-webdevelopment.com', + ); + + $sut = new StripeConfiguration( + $apiConfig['apiKey'], + $apiConfig['apiSecret'], + $apiConfig['redirectUrl'] + ); + + $this->assertSame($apiConfig['apiKey'], $sut->clientId()); + $this->assertSame($apiConfig['apiSecret'], $sut->clientSecret()); + $this->assertSame($apiConfig['redirectUrl'], $sut->redirectUrl()); + } +} diff --git a/tests/ThirdParty/Stripe/StripeConnectionFactoryTest.php b/tests/ThirdParty/Stripe/StripeConnectionFactoryTest.php new file mode 100644 index 0000000..2c159af --- /dev/null +++ b/tests/ThirdParty/Stripe/StripeConnectionFactoryTest.php @@ -0,0 +1,24 @@ + + * @date 15-02-2021 + */ + +namespace TSK\SSO\ThirdParty\Stripe; + +use PHPUnit\Framework\TestCase; + +class StripeConnectionFactoryTest extends TestCase +{ + /** + * @test + */ + public function shouldReturnAnInstanceOfAStripeConnection() + { + $sut = new StripeConnectionFactory(); + + $actualConnection = $sut->get('clientId', 'clientSecret', 'www.tsk-webdevelopment.com'); + + $this->assertInstanceOf('\TSK\SSO\ThirdParty\Stripe\StripeConnection', $actualConnection); + } +} diff --git a/tests/ThirdParty/ThirdPartyConnectionCollectionTest.php b/tests/ThirdParty/ThirdPartyConnectionCollectionTest.php index b1bbe28..bf79108 100644 --- a/tests/ThirdParty/ThirdPartyConnectionCollectionTest.php +++ b/tests/ThirdParty/ThirdPartyConnectionCollectionTest.php @@ -1,46 +1,46 @@ - - * @date 01-01-2019 - */ - -namespace TSK\SSO\ThirdParty; - -use Mockery; -use PHPUnit\Framework\TestCase; - -class ThirdPartyConnectionCollectionTest extends TestCase -{ - /** - * @test - */ - public function shouldBeAbleToRetrieveTheAddedConnectionByName() - { - $connectionMock1 = Mockery::mock('\TSK\SSO\ThirdParty\VendorConnection'); - $connectionMock2 = Mockery::mock('\TSK\SSO\ThirdParty\VendorConnection'); - - $sut = new ThirdPartyConnectionCollection(); - - $sut->add('VENDOR_1', $connectionMock1); - $sut->add('VENDOR_2', $connectionMock2); - - $this->assertSame($connectionMock1, $sut->getByVendor('VENDOR_1')); - $this->assertSame($connectionMock2, $sut->getByVendor('VENDOR_2')); - } - - /** - * @test - * @expectedException \TSK\SSO\ThirdParty\Exception\UnknownVendorRequestException - * @expectedExceptionMessage Given vendor 'VENDOR_2' is not yet configured - */ - public function shouldThrowExceptionOnNonExistingConnectionTypeRequest() - { - $connectionMock1 = Mockery::mock('\TSK\SSO\ThirdParty\VendorConnection'); - - $sut = new ThirdPartyConnectionCollection(); - $sut->add('VENDOR_1', $connectionMock1); - - // NOTE: requesting for a type that we didn't push - $sut->getByVendor('VENDOR_2'); - } -} + + * @date 01-01-2019 + */ + +namespace TSK\SSO\ThirdParty; + +use Mockery; +use PHPUnit\Framework\TestCase; + +class ThirdPartyConnectionCollectionTest extends TestCase +{ + /** + * @test + */ + public function shouldBeAbleToRetrieveTheAddedConnectionByName() + { + $connectionMock1 = Mockery::mock('\TSK\SSO\ThirdParty\VendorConnection'); + $connectionMock2 = Mockery::mock('\TSK\SSO\ThirdParty\VendorConnection'); + + $sut = new ThirdPartyConnectionCollection(); + + $sut->add('VENDOR_1', $connectionMock1); + $sut->add('VENDOR_2', $connectionMock2); + + $this->assertSame($connectionMock1, $sut->getByVendor('VENDOR_1')); + $this->assertSame($connectionMock2, $sut->getByVendor('VENDOR_2')); + } + + /** + * @test + * @expectedException \TSK\SSO\ThirdParty\Exception\UnknownVendorRequestException + * @expectedExceptionMessage Given vendor 'VENDOR_2' is not yet configured + */ + public function shouldThrowExceptionOnNonExistingConnectionTypeRequest() + { + $connectionMock1 = Mockery::mock('\TSK\SSO\ThirdParty\VendorConnection'); + + $sut = new ThirdPartyConnectionCollection(); + $sut->add('VENDOR_1', $connectionMock1); + + // NOTE: requesting for a type that we didn't push + $sut->getByVendor('VENDOR_2'); + } +} diff --git a/tests/ThirdParty/ThirdPartyUserTest.php b/tests/ThirdParty/ThirdPartyUserTest.php index e9ff14a..7277b96 100644 --- a/tests/ThirdParty/ThirdPartyUserTest.php +++ b/tests/ThirdParty/ThirdPartyUserTest.php @@ -1,40 +1,40 @@ - - * @date 01-01-2019 - */ - -namespace TSK\SSO\ThirdParty; - -use PHPUnit\Framework\TestCase; - -class ThirdPartyUserTest extends TestCase -{ - /** - * @test - */ - public function shouldReturnWithInstantiatedValues() - { - $vendorUser = array( - 'id' => 'vendor-id', - 'name' => 'test-vendor', - 'email' => 'vendor-email@test.com', - 'pictureUrl' => 'http://pictures/picture.jpg', - 'gender' => 'the-gender', - ); - - $sut = new ThirdPartyUser( - $vendorUser['id'], - $vendorUser['name'], - $vendorUser['email'], - $vendorUser['pictureUrl'], - $vendorUser['gender'] - ); - - $this->assertSame($vendorUser['id'], $sut->id()); - $this->assertSame($vendorUser['name'], $sut->name()); - $this->assertSame($vendorUser['email'], $sut->email()); - $this->assertSame($vendorUser['pictureUrl'], $sut->avatar()); - $this->assertSame($vendorUser['gender'], $sut->gender()); - } -} + + * @date 01-01-2019 + */ + +namespace TSK\SSO\ThirdParty; + +use PHPUnit\Framework\TestCase; + +class ThirdPartyUserTest extends TestCase +{ + /** + * @test + */ + public function shouldReturnWithInstantiatedValues() + { + $vendorUser = array( + 'id' => 'vendor-id', + 'name' => 'test-vendor', + 'email' => 'vendor-email@test.com', + 'pictureUrl' => 'http://pictures/picture.jpg', + 'gender' => 'the-gender', + ); + + $sut = new ThirdPartyUser( + $vendorUser['id'], + $vendorUser['name'], + $vendorUser['email'], + $vendorUser['pictureUrl'], + $vendorUser['gender'] + ); + + $this->assertSame($vendorUser['id'], $sut->id()); + $this->assertSame($vendorUser['name'], $sut->name()); + $this->assertSame($vendorUser['email'], $sut->email()); + $this->assertSame($vendorUser['pictureUrl'], $sut->avatar()); + $this->assertSame($vendorUser['gender'], $sut->gender()); + } +}