diff --git a/lib/Alchemy/Phrasea/Authentication/Provider/AbstractProvider.php b/lib/Alchemy/Phrasea/Authentication/Provider/AbstractProvider.php new file mode 100644 index 0000000000..4264b959c9 --- /dev/null +++ b/lib/Alchemy/Phrasea/Authentication/Provider/AbstractProvider.php @@ -0,0 +1,66 @@ +generator = $generator; + + return $this; + } + + /** + * @return UrlGenerator + */ + public function getUrlGenerator() + { + return $this->generator; + } + + /** + * @param SessionInterface $session + * + * @return ProviderInterface + */ + public function setSession(SessionInterface $session) + { + $this->session = $session; + + return $this; + } + + /** + * @return SessionInterface + */ + public function getSession() + { + return $this->session; + } + + protected function createState() + { + return md5(uniqid(microtime(true), true)); + } +} diff --git a/lib/Alchemy/Phrasea/Authentication/Provider/Facebook.php b/lib/Alchemy/Phrasea/Authentication/Provider/Facebook.php new file mode 100644 index 0000000000..8a3de9b7cc --- /dev/null +++ b/lib/Alchemy/Phrasea/Authentication/Provider/Facebook.php @@ -0,0 +1,198 @@ +facebook = $facebook; + $this->generator = $generator; + } + + /** + * {@inheritdoc} + */ + public function getId() + { + return 'facebook'; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'Facebook'; + } + + /** + * {@inheritdoc} + */ + public function authenticate() + { + return new RedirectResponse($this->facebook->getLoginUrl(array( + 'scope' => 'email', + 'redirect_uri' => $this->generator->generate( + 'login_authentication_provider_callback', + array('providerId' => $this->getId()), + UrlGenerator::ABSOLUTE_URL + ) + ))); + } + + /** + * @param \Facebook $facebook + * + * @return Facebook + */ + public function setFacebook(\Facebook $facebook) + { + $this->facebook = $facebook; + + return $this; + } + + /** + * @return \Facebook + */ + public function getFacebook() + { + return $this->facebook; + } + + /** + * {@inheritdoc} + */ + public function getIdentity() + { + try { + $data = $this->facebook->api('/me'); + $identity = new Identity(); + + $identity->set(Identity::PROPERTY_ID, $data['id']); + $identity->set(Identity::PROPERTY_IMAGEURL, sprintf( + 'https://graph.facebook.com/%s/picture?return_ssl_resources=1', + $data['username'] + )); + $identity->set(Identity::PROPERTY_EMAIL, $data['email']); + $identity->set(Identity::PROPERTY_FIRSTNAME, $data['first_name']); + $identity->set(Identity::PROPERTY_LASTNAME, $data['last_name']); + $identity->set(Identity::PROPERTY_USERNAME, $data['username']); + + } catch (\FacebookApiException $e) { + throw new RuntimeException('Unable to get profile informations', $e->getCode(), $e); + } + + return $identity; + } + + /** + * {@inheritdoc} + */ + public function onCallback(Request $request) + { + if (!$this->facebook->getUser()) { + throw new NotAuthenticatedException('Facebook authentication failed'); + } + } + + /** + * {@inheritdoc} + */ + public function getToken() + { + if (0 >= $this->facebook->getUser()) { + throw new RuntimeException('Provider has not authenticated'); + } + + return new Token($this, $this->facebook->getUser()); + } + + /** + * {@inheritdoc} + */ + public function getIconURI() + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADEAAAAwCAYAAAC4w' + . 'JK5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0W' + . 'E1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iV' + . 'zVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iY' + . 'WRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExI' + . 'DY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSR' + . 'EYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1ze' + . 'W50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6e' + . 'G1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0U' + . 'mVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZ' + . 'WYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtc' + . 'E1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDowMjgwMTE3NDA3MjA2ODExO' + . 'EMxNEY3NEJCM0UzNjU4QyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpFRjg3O' + . 'UMwQTcxRUMxMUUyQjc5NzlGRUJDNjcwRkVDMyIgeG1wTU06SW5zdGFuY2VJRD0ie' + . 'G1wLmlpZDpFRjg3OUMwOTcxRUMxMUUyQjc5NzlGRUJDNjcwRkVDMyIgeG1wOkNyZ' + . 'WF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpIj4gPHhtc' + . 'E1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDI4MDExN' + . 'zQwNzIwNjgxMThDMTRGNzRCQjNFMzY1OEMiIHN0UmVmOmRvY3VtZW50SUQ9Inhtc' + . 'C5kaWQ6MDI4MDExNzQwNzIwNjgxMThDMTRGNzRCQjNFMzY1OEMiLz4gPC9yZGY6R' + . 'GVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlb' + . 'mQ9InIiPz59EDiBAAAEEUlEQVR42txaXUgUURQ+szu76pZlYhoJJoYaEgUJRUUUQ' + . 'hBBL0FPUdBjEUFPPfTaSw9B0GsR0UuPkQ8FQdFDVFBKJGaFS6b4r6Vru87fvZ0zM' + . '2uz7o6Ozr02duXszs4MM+e733fuOfdeFc45rPcWg/+gqfShKIr95RoBi3uOo9YYG' + . 'snHyh+r7gXb4TsvP7RX1Wy7r2tmm6GbEEWlMcvsS/e8v3jr0tku/KmjGV4QiQ3Vt' + . 'Q8nx+dapoZ/QTYzD1GMF1TNrmR5wz087EDLoGVVT2wk9RxrmRj6CRND06DPG1EFA' + . 'cnyRBMe1rmnzDwIioEyPWdAdjYHWlaHqI5aHP/IP2y1xIKXCVtODP228INFfNh1O' + . '3gzWor89sopzizEyZht66BVUAiQ36o3ZyAGIP/XionaLSk4sq8B2pq2QnVVCjZWJ' + . 'G256IYFmmbC5/Qk3H3cvVR6oM6PeUGAhShsOTG5IBJqDM6f2gMdB3ZCPKb43pdB7' + . 'QfxpQAEsUB6kxnU8XgMrl04DLub65a9NxdwhCxiguJCJhOnO5oDAbD9CagKHzkxa' + . 'TI6ebQ18P2mxQL5sggE2MhlMdHaWAOVqbKS1wZHZ+DT11HIooQWzo3MrI4JmXKqr' + . '60seX5sag6u3nwKhrk6BRSCMEmHTJqc1Hjporj/xzRoWHCGKsX/apA7eUISE9znu' + . 'QQgzDtLyEkeE35JFGuEUO8sBEG1E5dXO/k+loerEgpBGAgAg4vYENHKkyqoarxgi' + . 'C2ZADFrp8oSxQwhO97RKhAILFmABghLUExcOXcQjh9qXva+Y/ubbFvcevvH4fKNz' + . 'pWB4IKr2LCyzOH8hq802RkIgrKkJQhE2NDKaUYgXxblCbFDbNhCMpPVVpmxBSa7s' + . 'J3x2y7FV8iEPbOzKCYsISDedA/AbEZb+N3SVAPtbfVF96UHp+Hdx8Gi8129Q4F8K' + . 'SoARU6Knr/+Ylu+nTmxtySInm8jcPvBKzF5wmYBkYhiotT8wG8UC/POEjEhbz7h9' + . '9yw71wUE86LuDQQSzHBBDIhswD0eS4XyoRJPeJfMstlQmApbqGmmKTAXjomBAU2d' + . '3uE838QE1xgYNsgZMnJpwzhYuXkSEmanKw1kBNNiJxyXNbMbg2Y4BxZQJMWE34gm' + . 'NCYYHJjYi2GWKpf7JmdJCb85QTimHBWOpi01Q7fjB1yhUWF4jHWMQmt89lb20Q3t' + . 'WBwMnIDHJQdTqdEd9/OMrKT+EXrntTbLL8QRD+s0b4n1zHAhpRYcqGGipqZ83NTk' + . '+kXj2gdAZzNeEshPSqKQuvtm8DZG25E245WA87uZDxKJICz7UtMDKN9RxtTPRdpM' + . 'kw79OP5eTo4O5RqhECYLgMzrp/kr6Z65KS7J8FFSyzQFmuU/kkl72fW9ZVM/yPAA' + . 'LuRXOCVA9oFAAAAAElFTkSuQmCC'; + } + + /** + * {@inheritdoc} + * + * @return Facebook + */ + public static function create(UrlGenerator $generator, SessionInterface $session, array $options) + { + $config['appId'] = $options['app-id']; + $config['secret'] = $options['secret']; + + $facebook = new \Facebook($config); + + return new static($facebook, $generator); + } +} diff --git a/lib/Alchemy/Phrasea/Authentication/Provider/Github.php b/lib/Alchemy/Phrasea/Authentication/Provider/Github.php new file mode 100644 index 0000000000..605944c356 --- /dev/null +++ b/lib/Alchemy/Phrasea/Authentication/Provider/Github.php @@ -0,0 +1,271 @@ +generator = $generator; + $this->session = $session; + $this->client = $client; + + $this->key = $key; + $this->secret = $secret; + } + + /** + * @param ClientInterface $client + * + * @return Github + */ + public function setGuzzleClient(ClientInterface $client) + { + $this->client = $client; + + return $this; + } + + /** + * @return ClientInterface + */ + public function getGuzzleClient() + { + return $this->client; + } + + /** + * {@inheritdoc} + */ + public function getId() + { + return 'github'; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'Github'; + } + + /** + * {@inheritdoc} + */ + public function authenticate() + { + $state = $this->createState(); + + $this->session->set('github.provider.state', $state); + + return new RedirectResponse('https://github.com/login/oauth/authorize?' . http_build_query(array( + 'client_id' => $this->key, + 'scope' => 'user,user:email', + 'state' => $state, + 'redirect_uri' => $this->generator->generate( + 'login_authentication_provider_callback', + array('providerId' => $this->getId()), + UrlGenerator::ABSOLUTE_URL + ), + ), '', '&')); + } + + /** + * {@inheritdoc} + */ + public function onCallback(Request $request) + { + if (!$this->session->has('github.provider.state')) { + throw new RuntimeException('Invalid state value ; CSRF try ?'); + } + + if ($request->query->get('state') !== $this->session->remove('github.provider.state')) { + throw new RuntimeException('Invalid state value ; CSRF try ?'); + } + + $guzzleRequest = $this->client->post('access_token'); + + $guzzleRequest->addPostFields(array( + 'code' => $request->query->get('code'), + 'redirect_uri' => $this->generator->generate( + 'login_authentication_provider_callback', + array('providerId' => $this->getId()), + UrlGenerator::ABSOLUTE_URL + ), + 'client_id' => $this->key, + 'client_secret' => $this->secret, + )); + $guzzleRequest->setHeader('Accept', 'application/json'); + $response = $guzzleRequest->send(); + + if (200 !== $response->getStatusCode()) { + throw new RuntimeException('Error while getting access_token'); + } + + $data = json_decode($response->getBody(true), true); + + $this->session->remove('github.provider.state'); + $this->session->set('github.provider.access_token', $data['access_token']); + + $request = $this->client->get('https://api.github.com/user'); + $request->getQuery()->add('access_token', $data['access_token']); + $request->setHeader('Accept', 'application/json'); + + $response = $request->send(); + + if (200 !== $response->getStatusCode()) { + throw new RuntimeException('Error while retrieving user info'); + } + + $data = json_decode($response->getBody(true), true); + + $this->session->set('github.provider.id', $data['id']); + } + + /** + * {@inheritdoc} + */ + public function getToken() + { + if ('' === trim($this->session->get('github.provider.id'))) { + throw new RuntimeException('Github has not authenticated'); + } + + return new Token($this, $this->session->get('github.provider.id')); + } + + /** + * {@inheritdoc} + */ + public function getIdentity() + { + $identity = new Identity(); + + $request = $this->client->get('https://api.github.com/user'); + $request->getQuery()->add('access_token', $this->session->get('github.provider.access_token')); + $request->setHeader('Accept', 'application/json'); + + $response = $request->send(); + + if (200 !== $response->getStatusCode()) { + throw new RuntimeException('Error while retrieving user info'); + } + + $data = json_decode($response->getBody(true), true); + + list($firstname, $lastname) = explode(' ', $data['name'], 2); + + $identity->set(Identity::PROPERTY_EMAIL, $data['email']); + $identity->set(Identity::PROPERTY_FIRSTNAME, $firstname); + $identity->set(Identity::PROPERTY_ID, $data['id']); + $identity->set(Identity::PROPERTY_IMAGEURL, $data['avatar_url']); + $identity->set(Identity::PROPERTY_LASTNAME, $lastname); + + return $identity; + } + + /** + * {@inheritdoc} + */ + public function getIconURI() + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADEAAAAwCAYAAAC4w' + . 'JK5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MO' + . 'mNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ' + . '2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6b' + . 'WV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgM' + . 'jAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJod' + . 'HRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZ' + . 'XNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZ' + . 'S5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL' + . '3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZ' + . 'G9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZ' + . 'DowMjgwMTE3NDA3MjA2ODExOEMxNEY3NEJCM0UzNjU4QyIgeG1wTU06RG9jdW1lbnRJR' + . 'D0ieG1wLmRpZDowMUNGRURDNTgzRUUxMUUyQjNFMDk5RUI0NTk2RTdBMiIgeG1wTU06S' + . 'W5zdGFuY2VJRD0ieG1wLmlpZDowMUNGRURDNDgzRUUxMUUyQjNFMDk5RUI0NTk2RTdBM' + . 'iIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpI' + . 'j4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6ODUwR' + . 'DRBRkIxQjIwNjgxMThDMTRGNzRCQjNFMzY1OEMiIHN0UmVmOmRvY3VtZW50SUQ9Inhtc' + . 'C5kaWQ6MDI4MDExNzQwNzIwNjgxMThDMTRGNzRCQjNFMzY1OEMiLz4gPC9yZGY6RGVzY' + . '3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiP' + . 'z4ThndjAAAG5ElEQVR42tRaaUxUVxT+GB+YIqjEiCwiVBoLGgSRRRHUxjSKVutSfzTGu' + . 'CXUWnFNbGpKXYgLKribaGLin1ZjiwUxbq1xqaICKqKyKJtSwAUdBpBtYKbn3HlDGRiYY' + . 'cRk5pLDzH3v3vPud89yzzlv7LRaLWy9SfzPzs5OfMikIOrT7rs1NU0HYglo7VgSdjoUi' + . 'sKnT8NcXV1P0LURGo3G6nZc3dyc/evJkwvXrlv3nLrNRC0MRg+Cd9xBpVQ+blGrfZsaG' + . '9DSQvetTdVor9+/f3/FP2D0EurVEjUwNkm+LUDQqgWAhvp6aFpbrVL/7SVpEn24yl1eZ' + . 'IseBNtAX1YhlkCrlQIQTauVZBANMjXpQbBN2LP2MBBr9ljy2gYSveE1sxYp2qlTHxvyq' + . 'o6sObJ3tZPa3VCwx2KkNnB2SPKmKzqCkL0urM8rGfFTnQ67/zHoUNjaKS51MhyZbBeEX' + . 'pUskAR7tbuZmfjryhVkP8xBeUUFamtr4ezsDE8PDwQFjsaXU6YgPDSU3IniY0rCMsO+f' + . 'uMfJO7fj6LiYtF3cHCA19ChGEbU2NSE4pIS5BcU4NTp3+E7fDjWr16NSROjPg4InRDMB' + . '6FWq7Fj126cTk4Wuzt92jTMmzMbwUFBsLe3Nxh378EDnElJxcXLl7GCQMyfOxcbf9xgM' + . 'M5iK5djJ/a7g6tevSpVvn2LxoZ6swCsWrceN9PT8ZmvL3bEx8Pv8xEm5+UXPMVPcXEoL' + . 'CpCZEQEDiQl9hhIwNiQ7+kjl4gDwTcKo2atNU3bExIEgNCxY3H00EF86j3MrHmf+njj2' + . 'OFDCAsJEfO37Uwwa54B9YY63bh5C3/8mQIfb2/s37Mbu5L24sKlS5gRPQ0/LF8u4q+ys' + . 'n/R0NgIR8dPMNTTE5Ik4cjRYzh3/jxmz5qFfbt3YcGSpUhOScHkiRMxKSqy91wsTLhY9' + . 'kJ7Dx4UidT2rVvQz8kJz8vK0NTcjDOpZ5GSdg7GchG2Gf315y9eiHnbaP6CRYuJ3wFET' + . 'oiw2Gt1VicToswgN8peiN3lKH9/cc118GADkF2B17dBgwaJeaP8/ASf4pJS3M3ItFidF' + . 'B3VSUP/uqO/r14VY+fN/lr0ldXVyMjK7NHO8UYoVSoxn/lwu3Ltqsln68kMSaDbXXiY8' + . 'wgO5E2CAgNFPzUtDUpldY9AvKmqwlmax/PHEB/ml/PoUS8atonYqbKyEh50AlOGJcY9z' + . 's2zSI9z8/LFfDZ45ldeUWlxzKYwlnR0R7V1dehPoYS+z+eFJa2Fskc9jwH9+6OO+Jp6d' + . 'lees0PsJFI7aLupdDiTV6mhmEg/xtPD3SIQ7kOGtPFQ1dTAyalft881WxKsSqaMys3ND' + . 'RUU3PEZwP3I8REWPThi3Dgxn/kwPw93j94x7DaRydIwRgGjRqKZVCg7J0f0g4MCEREe3' + . 'iMAk6OixDyez3yYX8DIkd0+tz2ZdLFaE7vApyu3FPIurAbX6fSO3/QLncIzhZF2W26xl' + . '/DNnDnYEvdzGz/mwy0qMtJiSXSyCa0Jm+AI1YfipKvXb+Db+fPx4GE2bt+5gw1r1yBm8' + . 'WJk3r+P/YeP4J1S2TbHe9gwLFu0CCHBYzBwwADdo+gZhXRoXqMwnvnxvV6xiY7xkzHi5' + . 'HZFTIw4gXfuScTShQtxLzub8okDyHn8hPz9YwMA3F5QWDKcAj/2Qno+bAtbt+8QNS7mZ' + . '2eGZzTLO+kGaUzuSERYGGZGRyPtwgVs3LwZqyjo25mUhOTU1C5rRYVFxfAhiXBrJAAbN' + . '28RkviKgkbmp/2A2q/UlXcy1dbErsTrqjeUkmahquotEuK3wsXFBXezspCQmGQ0dmK+B' + . 'c+eYWdiIsVfJSIcXxsba9bzepBji+DJrF2RKOLctmkT9h06jHMXLyImdhUmRU6Ay0AXo' + . '+Of5OXh5u10YQO86BlTpxKAlYKP9gMr8AaZXVlJSenLinLUkdfpSbt9NwNHjx8XIbmp5' + . 'u3lhe+WLcP48DCLF/1F9HSDzK6TOllSKBgXForw0BDcJwO/cSsdufl5qHz5CvX19ZQUO' + . 'cLdbQhG+vljIuUM7N04F+nN2lanko0pF9tdC6aIlMlERbjXi3OSsaTIpiuAbaVY2HIZs' + . 'y12smVJ9LB4ZqUFZfnE1mpsWxLQ2sLria5BaBobG8rIh3tZM4pqleoddG9N9S/k26JY7' + . 'rT+dvJUXJNaXSFR3M9hpdbK/pSq6nfHTpw4RWvlYnGzHow+7OCXeP2JhhD5cOrMNS7oX' + . 'vBZywvJVnnxb4nKiUqJXhHVSO0GNEH3lv61fK3OSkGo5DXWymtuNfhZhLxoZ5kc5WvW8' + . 'iMVjaxC9TKAWr1aGfxABbqX27zwvvL3PlYGgqWhliXQLH/X/CfAALHSg9r3u8gEAAAAA' + . 'ElFTkSuQmCC'; + } + + /** + * {@inheritdoc} + */ + public static function create(UrlGenerator $generator, SessionInterface $session, array $options) + { + if (!isset($options['client-id'])) { + throw new InvalidArgumentException('Missing GitHub client-id parameter'); + } + + if (!isset($options['client-secret'])) { + throw new InvalidArgumentException('Missing GitHub client-secret parameter'); + } + + return new Github($generator, $session, new Guzzle('https://github.com/login/oauth'), $options['client-id'], $options['client-secret']); + } +} diff --git a/lib/Alchemy/Phrasea/Authentication/Provider/GooglePlus.php b/lib/Alchemy/Phrasea/Authentication/Provider/GooglePlus.php new file mode 100644 index 0000000000..793fca809e --- /dev/null +++ b/lib/Alchemy/Phrasea/Authentication/Provider/GooglePlus.php @@ -0,0 +1,319 @@ +generator = $generator; + $this->session = $session; + $this->client = $google; + $this->guzzle = $guzzle; + + $this->plus = new \Google_PlusService($this->client); + + $this->client->setScopes(array( + 'https://www.googleapis.com/auth/plus.me', + 'https://www.googleapis.com/auth/userinfo.email', + )); + + $this->client->setRedirectUri( + $this->generator->generate( + 'login_authentication_provider_callback', array( + 'providerId' => $this->getId(), + ), UrlGenerator::ABSOLUTE_URL + ) + ); + + $this->client->setApprovalPrompt("auto"); + + if ($this->session->has('google-plus.provider.token')) { + $this->client->setAccessToken($this->session->get('google-plus.provider.token')); + } + } + + /** + * @param ClientInterface $client + * + * @return Github + */ + public function setGuzzleClient(ClientInterface $client) + { + $this->guzzle = $client; + + return $this; + } + + /** + * @return ClientInterface + */ + public function getGuzzleClient() + { + return $this->guzzle; + } + + /** + * @param \Google_Client $client + * + * @return GooglePlus + */ + public function setGoogleClient(\Google_Client $client) + { + $this->client = $client; + + return $this; + } + + /** + * @return \Google_Client + */ + public function getGoogleClient() + { + return $this->client; + } + + /** + * @param \Google_PlusService $plus + * + * @return GooglePlus + */ + public function setGooglePlusService(\Google_PlusService $plus) + { + $this->plus = $plus; + + return $this; + } + + /** + * @return \Google_PlusService + */ + public function getGooglePlusService() + { + return $this->plus; + } + + /** + * {@inheritdoc} + */ + public function getId() + { + return 'google-plus'; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'Google +'; + } + + /** + * {@inheritdoc} + */ + public function authenticate() + { + $state = $this->createState(); + + $this->session->set('google-plus.provider.state', $state); + $this->client->setState($state); + + return new RedirectResponse($this->client->createAuthUrl()); + } + + /** + * {@inheritdoc} + */ + public function onCallback(Request $request) + { + if (!$this->session->has('google-plus.provider.state')) { + throw new RuntimeException('Invalid state value ; CSRF try ?'); + } + + if ($request->query->get('state') !== $this->session->remove('google-plus.provider.state')) { + throw new RuntimeException('Invalid state value ; CSRF try ?'); + } + + $this->client->authenticate($request->query->get('code')); + + $token = json_decode($this->client->getAccessToken(), true); + $ticket = $this->client->verifyIdToken($token['id_token']); + + $this->session->set('google-plus.provider.token', json_encode($token)); + $this->session->set('google-plus.provider.id', $ticket->getUserId()); + } + + /** + * {@inheritdoc} + */ + public function getToken() + { + if (!ctype_digit($this->session->get('google-plus.provider.id'))) { + throw new RuntimeException('Google + has not authenticated'); + } + + return new Token($this, $this->session->get('google-plus.provider.id')); + } + + /** + * {@inheritdoc} + */ + public function getIdentity() + { + $identity = new Identity(); + + $token = json_decode($this->session->get('google-plus.provider.token'), true); + $request = $this->guzzle->get(sprintf( + 'https://www.googleapis.com/oauth2/v1/tokeninfo?%s', + http_build_query(array('access_token' => $token['access_token']), '', '&') + )); + $response = $request->send(); + + if (200 !== $response->getStatusCode()) { + throw new RuntimeException('Error while retrieving user info'); + } + + try{ + $plusData = $this->plus->people->get('me'); + } catch (\Google_Exception $e) { + throw new RuntimeException('Error while retrieving user info', $e->getCode(), $e); + } + + $data = json_decode($response->getBody(true), true); + + $identity->set(Identity::PROPERTY_EMAIL, $data['email']); + + $identity->set(Identity::PROPERTY_FIRSTNAME, $plusData['name']['givenName']); + $identity->set(Identity::PROPERTY_ID, $plusData['id']); + $identity->set(Identity::PROPERTY_IMAGEURL, $plusData['image']['url']); + $identity->set(Identity::PROPERTY_LASTNAME, $plusData['name']['familyName']); + + return $identity; + } + + /** + * {@inheritdoc} + */ + public function getIconURI() + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADEAAAAwCAYAAAC4w' + . 'JK5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MO' + . 'mNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ' + . '2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6b' + . 'WV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgM' + . 'jAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJod' + . 'HRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZ' + . 'XNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZ' + . 'S5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL' + . '3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZ' + . 'G9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZ' + . 'DowMjgwMTE3NDA3MjA2ODExOEMxNEY3NEJCM0UzNjU4QyIgeG1wTU06RG9jdW1lbnRJR' + . 'D0ieG1wLmRpZDo2N0M5MkQ3OTcxRUUxMUUyQjc5NzlGRUJDNjcwRkVDMyIgeG1wTU06S' + . 'W5zdGFuY2VJRD0ieG1wLmlpZDo2N0M5MkQ3ODcxRUUxMUUyQjc5NzlGRUJDNjcwRkVDM' + . 'yIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpI' + . 'j4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDI4M' + . 'DExNzQwNzIwNjgxMThDMTRGNzRCQjNFMzY1OEMiIHN0UmVmOmRvY3VtZW50SUQ9Inhtc' + . 'C5kaWQ6MDI4MDExNzQwNzIwNjgxMThDMTRGNzRCQjNFMzY1OEMiLz4gPC9yZGY6RGVzY' + . '3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiP' + . 'z6uF8PaAAAL1UlEQVR42txae4wdVRn/zdzXPu/d1227W5aWPizQFrolXZAFS3hUpICAC' + . 'oLVNlU0NcVoiTEm/mEqGMVUVBqEolFEBUQbiokIAo1Q6COlrZTutrRLu9vd7pbtvu7uf' + . 'c7j+J2ZM/eemZ177xITX5N8e86dM3PO9/ve58wqjDH8r18q/g+uIP+jKIrVCOLAAlL/v' + . '+ligkwiQ7Q2CIfhvr6+9ubm5l+rqvqRk0MZzHvkPaA5RJAEvjxWC3nhzWMZbLq2AVtun' + . 'O27cm73biSuvNJ61OHCkLgxpX6pe9TqOvBKG/A5+pkh0oh0hfsEaYJLPKzr+ruBQGA+X' + . '/jp3YO4Z9txYG4Vve1hPN9XCvLpz+DAd5ag7fxqXyDDK1Ygt3+/1TeLkOHz2wua00eBh' + . 'dSMEyWJsqrkG2EHAL8GEprdsWamV3kAyIuImDeVgpg4IHruyX3DRe0gfPPNtu597MNpF' + . 'YkcJTPpGQcYXc1EMaIKbvoOCO4DEZcJGMyzimIDsfpMIjF7Uxg/PTSK4UndH0RHBxTPP' + . 'UViVJGW8j7j048T1fJpuQJUaTwkvxwJ0pAhMeoiRawoWv5MBclhMIsDvZP+EWTBAktSx' + . 'aTPPGNKEY8W96NCC5xnRTangPxSvCZUmN10TMrbd0ysAOaNbn8QTPInVWLSy7RXI8yjN' + . 'WGSEQHAmipYLGe0NtBzXBucUc6A4kifSStJrPDfoQD29qZ9QSiG4csYfJiWmPXVkldRw' + . 'WKOePF5FJXqyeRSNF2l4jFOaXlFmjekYChlWNaleOzBOHXKYkzxMQ+HaW+fTcPMSmbse' + . 'E0Yay6mAPBBpqAFeUbmMSnB0UjOgGZMddHczp0uiSvCfsPCQ+tEuIlKHhsoYV7T0gS/1' + . 'qyYgd++NGA7eF63ir9MFGY9F6EmGJg6l/6n7agRTDpXlmiU6GxdBNnKAGUtkikzEUgai' + . 'CayaJAY5EY6JhLDhwLx8aX1WHppPQ53Ul5pqRKGKlSiSGCcTE6md15tCKrXlh77Heq6O' + . 'q3s9GrHbHTOacTp+gqciVWgl+h4LIw0BUqdXlNJw1W6ifmJHFrGMoiPZzFrNI25p0dw0' + . 'e5+tJgfEgS/fnBLK1bvOQc0UPwPBwvJzXFsp88Nc1zDJ5fE3BOcHMSrP1qD7fctx74FM' + . '7B/NumjKmRrl5uiRlxlDSkHARM019nGSvKxeppXeEpaw8V3TGB53wTw8G6PEdhlRxV3A' + . '+qf8gOy6dmTePhX3cAiMoaQ6o5MHATPmafTqG6pxMADS1FbYdtTwsjiU4/eiVf0E/TuU' + . 'mCSqoCUBlVjNm8M7ujmm86Z7Xb8heqQLYA7nttAA51EPURDLk0kciai4am+/uO7LsBkU' + . 'scTf+gljVDojYftFMMBpEhDAxmoTRV4ZePCPICzmQRu3b4B+3KHgRmXINifcE/qmIV3P' + . '8PcAKzoQ43Knx+jtci09FLR6YatnTh5LuNrVtvWL8TW+y9C+zwq8IZyVPCRq51Joy6i4' + . 't5PtODoQ5fiirl28bd36Dgu+81nsK9nFwKxxQiks+SvrECG0zfd9wx53PQ8S0SAmY/KX' + . 'OaEL+w5VRnK4edrFmDtNc1F/eTt7gm8N5hGtCKIyxdG0RQtKPT1gSNY+dQdloQDDQvI9' + . 'nWfbMWKx01W3ryMr79W3JwwrxLp99JYt+Vd/OL1Qdy6rBHXUoRqm1NDVVYh4lw2v9Yi7' + . '9U91oeVz6yzTCVQTwWxrk0FUM58GMqaV+noxCNFjByH6qZd/xjBrt0UlRojmN9ajZnRM' + . 'KojASR6Urhr9Wx8Y9VUTf3k4LMkly4EWjtormxp6X4o7bDimW4KCB72dPFkIxWJTSR9i' + . 'iTdJyfRnaP7WQKZMbH5ywt9J+ubHCIvq6Q6SaN1zfIMytKd7jgrV3aYcmUqQPGtKSUwr' + . 'hHe1rU1YNXimC+I2+ZdTfE8AT0zKUoSmoQXfoYp+qa7bxpSXyImxpjhuSfasiDy9ZBPb' + . 'USA0mM5DDm7Ps+1dvFqbLjp+7Sv6IKRHBYllmmTKbVW35D60n1OhhgzzEIr+haQaWvCt' + . 'V8QoMgnsqeS+GvXeNHI9eh19+OhOx+ltHsO5nCPvcQU6Rtu6bs0ZfhozITCyRozyoEwB' + . 'cNe9TqaoX6Fggef7ytZqnyz/fN44YvPobKiAeaZLnpdEXMZPkybBbMrAk7J91mhGC2tC' + . 'anU5k4+odvleD/Vjz1EGR3HXujFmm0nUOrs8Jb5V+HI117G4jntYKffscwh72cOOUzJD' + . 'JqF+4ppk1crZQpAxk8IKLVnrSjE9821rTWYNyOG1sYwmusqMCteiUUNlQhWRnw3KPJ1Q' + . 'W0c79z3F1zzxD144+CfgeZF9I7iCZnFQ6vriLVEdHKDOEqSDmloa2vEygvrcfmFdVh1S' + . 'RwN1f/aOenr9/4eVzz+Wew99CIUAmJHnhKhtRjTjJUHse6uZtoIxXEdZWn3NQ7t9IvIj' + . 'XVBT/XDTA+CZSn6ZMl5NR3h+ApEr3ycduz1RcHs+cozaHnwagz0H4PS1GqbBjy73FJ5o' + . 'YQmSpbiyRNPItn9NLSh/dASw9bJlXVmELAPe1QhApYiBTbFUfexbQjPvq0okH2Dx3D5A' + . 'yupnK4jwBF/AL7acJuVueVdV+3ku8c2Uu9jYEcHzuxYh0TnS9AmhxGoiiAQrUKglqimC' + . 'kpVFViY2gj9bqyBNjaEoRduR/Lgt4qCaJ+1CDd13A2cOyOqAx42Tau1I5Xk9DrLPzPlX' + . 'rmDAm3sEE4+tQxjXW8hGA0hGCMlhSrJjFUiZhHTbYITNanmUmuqoVBiH975EMZ2FtfG2' + . 'ktWgz/IqLpVGCtkYeaTmS3fMadm7XIgep+/AZnhCYSbiHEWsBkXqYI5KcNJJwKUST8ML' + . 'rFgFYL1YSTe3oGRl6/3BdHGHTsap/IkXZCyLktehFR5zPCMlQKR6NqKyd5zCBMjBkmXM' + . '2eKlG84zFKri9YUcZ0Zjlb4xiVIZXgE4wdexcTb901ZcG6sGfXVFACyual1Uz4fMN+8k' + . 'R8rBWKi5wmQ8Eniql17SfWXxaCoyeBolhVM2QLsEE0bqFUwtncrjMQx14KhQBBV3Kn5l' + . 'wZTOpg2i7RyteDcKwVCT49YJ5x5E5KrBMEoj2b5AtMoVAt5wGKbiXAFcrStTh5/zLVgM' + . 'pfG2GTCDnGmT/b2trIGipQdQXfpZFhhVKcHVee00rTPBJh0/Ep6sk8r7PwLptpnyvlwq' + . 'YiTFi6YRK9rwSND7yM5SvuOQMjNEJvGZmk6myIlGIOWGYAaEeehrHC4p0qHf873FQVWj' + . 'rFOIphaOMFRrWeYrSXTvfKbPQeprCGNNzXbkvVjshSAcpui6pabkEvZNYtj305flwtay' + . 'Sf4KYTpmJgYswOKAoN8N1A907XgL3f90VpWtfg3i/uCWWKsFIjGZd9FOEYbnxG+vVQLF' + . 'bJpM+vYP4RPyM7NgciOb6ZylCBJMIu+lJ9/+9G/48ibf6N9fKOwc8/HOkPaUZp+YwJIK' + . 'RBqqBYLP70VGoXwNFWy3DMcodjRT4RZ4V+Oc+cjmXVGRC2pLfmBiWjbagrXl+Wr/PU/2' + . '2DVKgo5jGtH52oN/90gK5xRlUt2rO7Cr2Lx+i3Wl51Ef5b2y1req/NadkKpZHa8b5A/5' + . 'NJZJAc01C1ZhFnX78hPvOx7N2L8/RNQGhqFBLzfd6UtcdGx8powR0dHD/NOw5JNWL5pD' + . '+KXtlGkUjAxqCF1TkNuQoOW1SgRGiRtg8aIcjr5kYYsmWBqIEfaUBDvWIUL1h6l2e0jz' + . 'at+eDsO73oNSnwW1TWGzYju2RQVC7WGO7xS4h3xfC3OV7H8I17Dxo0b2zdv3vxIfX39e' + . 'Q6y1NABDB/cionT+6CND0JPUlme0vNfg0IUKcOxKMK0Aao9fzma2r+NcEOb9e6u3oO4e' + . '8t69HUeAWbOpNSgFiRZpEItta8gsxrBofFncCL5Ft05zk+JiEYcEGHx/YOHkjlEHEQjD' + . '1jeD5L/wYtLP8W/6/NP/0R823CWV0tBaXed45UHr8/FPf4ZtGo63zD+zSD4UcsHgld+z' + . 'GjIILJiAOLhIfGp9b/ln1QcQacEnxPit/lPAQYAZxcBzaHB/oIAAAAASUVORK5CYII='; + } + + /** + * {@inheritdoc} + */ + public static function create(UrlGenerator $generator, SessionInterface $session, array $options) + { + $client = new \Google_Client(); + + $client->setApplicationName('Phraseanet'); + $client->setClientId($options['client-id']); + $client->setClientSecret($options['client-secret']); + + return new GooglePlus($generator, $session, $client, new Guzzle()); + } +} diff --git a/lib/Alchemy/Phrasea/Authentication/Provider/Linkedin.php b/lib/Alchemy/Phrasea/Authentication/Provider/Linkedin.php new file mode 100644 index 0000000000..0d984bb93c --- /dev/null +++ b/lib/Alchemy/Phrasea/Authentication/Provider/Linkedin.php @@ -0,0 +1,259 @@ +generator = $generator; + $this->session = $session; + $this->client = $client; + + $this->key = $key; + $this->secret = $secret; + } + + /** + * @param ClientInterface $client + * + * @return Github + */ + public function setGuzzleClient(ClientInterface $client) + { + $this->client = $client; + + return $this; + } + + /** + * @return ClientInterface + */ + public function getGuzzleClient() + { + return $this->client; + } + + /** + * {@inheritdoc} + */ + public function getId() + { + return 'linkedin'; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'LinkedIN'; + } + + /** + * {@inheritdoc} + */ + public function authenticate() + { + $state = $this->createState(); + + $this->session->set('linkedin.provider.state', $state); + + return new RedirectResponse('https://www.linkedin.com/uas/oauth2/authorization?' . http_build_query(array( + 'response_type' => 'code', + 'client_id' => $this->key, + 'scope' => 'r_basicprofile r_emailaddress', + 'state' => $state, + 'redirect_uri' => $this->generator->generate( + 'login_authentication_provider_callback', + array('providerId' => $this->getId()), + UrlGenerator::ABSOLUTE_URL + ), + ), '', '&')); + } + + /** + * {@inheritdoc} + */ + public function onCallback(Request $request) + { + if (!$this->session->has('linkedin.provider.state')) { + throw new RuntimeException('Invalid state value ; CSRF try ?'); + } + + if ($request->query->get('state') !== $this->session->remove('linkedin.provider.state')) { + throw new RuntimeException('Invalid state value ; CSRF try ?'); + } + + $guzzleRequest = $this->client->post('https://www.linkedin.com/uas/oauth2/accessToken?' . http_build_query(array( + 'grant_type' => 'authorization_code', + 'code' => $request->query->get('code'), + 'redirect_uri' => $this->generator->generate( + 'login_authentication_provider_callback', + array('providerId' => $this->getId()), + UrlGenerator::ABSOLUTE_URL + ), + 'client_id' => $this->key, + 'client_secret' => $this->secret, + ), '', '&')); + $response = $guzzleRequest->send(); + + if (200 !== $response->getStatusCode()) { + throw new RuntimeException('Error while getting access_token'); + } + + $data = json_decode($response->getBody(true), true); + $this->session->remove('linkedin.provider.state'); + $this->session->set('linkedin.provider.access_token', $data['access_token']); + + $request = $this->client->get('https://api.linkedin.com/v1/people/~:(id,first-name,last-name,positions,industry,picture-url,email-address)'); + $request->getQuery() + ->add('oauth2_access_token', $data['access_token']) + ->add('format', 'json'); + + $response = $request->send(); + + if (200 !== $response->getStatusCode()) { + throw new RuntimeException('Error while retrieving user info'); + } + + $data = json_decode($response->getBody(true), true); + + $this->session->set('linkedin.provider.id', $data['id']); + } + + /** + * {@inheritdoc} + */ + public function getToken() + { + if ('' === trim($this->session->get('linkedin.provider.id'))) { + throw new RuntimeException('Linkedin has not authenticated'); + } + + return new Token($this, $this->session->get('linkedin.provider.id')); + } + + /** + * {@inheritdoc} + */ + public function getIdentity() + { + $identity = new Identity(); + + $request = $this->client->get('https://api.linkedin.com/v1/people/~:(id,first-name,last-name,positions,industry,picture-url;secure=true,email-address)'); + $request->getQuery() + ->add('oauth2_access_token', $this->session->get('linkedin.provider.access_token')) + ->add('format', 'json'); + + $response = $request->send(); + + if (200 !== $response->getStatusCode()) { + throw new RuntimeException('Error while retrieving user info'); + } + + $data = json_decode($response->getBody(true), true); + + if (0 < $data['positions']['_total']) { + $position = array_pop($data['positions']['values']); + $identity->set(Identity::PROPERTY_COMPANY, $position['company']['name']); + } + + $identity->set(Identity::PROPERTY_EMAIL, $data['emailAddress']); + $identity->set(Identity::PROPERTY_FIRSTNAME, $data['firstName']); + $identity->set(Identity::PROPERTY_ID, $data['id']); + $identity->set(Identity::PROPERTY_IMAGEURL, $data['pictureUrl']); + $identity->set(Identity::PROPERTY_LASTNAME, $data['lastName']); + + return $identity; + } + + /** + * {@inheritdoc} + */ + public function getIconURI() + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADEAAAAwCAYAAAC4w' + . 'JK5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MO' + . 'mNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ' + . '2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6b' + . 'WV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgM' + . 'jAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJod' + . 'HRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZ' + . 'XNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZ' + . 'S5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL' + . '3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZ' + . 'G9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZ' + . 'DowMjgwMTE3NDA3MjA2ODExOEMxNEY3NEJCM0UzNjU4QyIgeG1wTU06RG9jdW1lbnRJR' + . 'D0ieG1wLmRpZDo2N0M5MkQ3RDcxRUUxMUUyQjc5NzlGRUJDNjcwRkVDMyIgeG1wTU06S' + . 'W5zdGFuY2VJRD0ieG1wLmlpZDo2N0M5MkQ3QzcxRUUxMUUyQjc5NzlGRUJDNjcwRkVDM' + . 'yIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpI' + . 'j4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDI4M' + . 'DExNzQwNzIwNjgxMThDMTRGNzRCQjNFMzY1OEMiIHN0UmVmOmRvY3VtZW50SUQ9Inhtc' + . 'C5kaWQ6MDI4MDExNzQwNzIwNjgxMThDMTRGNzRCQjNFMzY1OEMiLz4gPC9yZGY6RGVzY' + . '3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiP' + . 'z4zg+u0AAAD+0lEQVR42uxaSWgTURj+kkwWS7VirdZaS6Xa2tqCW3FBq4IHl0MPFT14c' + . 'wE9K+hBD3oTRD1UPAgeBOsCgoinFulBQRGt2orSWqS2VatNapqmS5Z543uTyTKTTDIJI' + . '32RPvjJvLxl3jffv7z/zVgkSUK+Fyv+gyJELyy0JACzKb8WRXgqTHVIoghJoPqHHiEk7' + . 'kUgZAdTNd60TSIe/Bm7jj3bWmktQCVsidoEJYI9fSc+DUxieBTw+GizyJ/uML1w2MM4t' + . 'LuGXtFFYjKRCQbChUAQcHuB32OUKE6NXrCxdZcqtbCgMXKHvHDGgMix1wrJGlLCWNAyw' + . 'YiK2wHh3vUWUZnHeNGCiLhcGQj3IFzyQ9eAQBwAiQj/Mc6iihMqL8yAkPyJ5ELKWBIFk' + . 'rcgokad/0wYswmHzYpj66pRt7gIXSNjuNPdTz2zxAMTkmGbaGtpQkttZaxeX7IQp9tfc' + . '7CLJVF1ImnFSoE211Sohh5YVZ5xnGmSFoQoxW0ijRCR4O0Pt2po10+3obGmSEZ1IsZso' + . 'uVeB87vWo/VxUV4/9ODS51dsxJfkkGEjXun714/Tj1+zqF3iqlT5id6ZN1qlBcVxurvq' + . 'Hq1fxmSr8/uXK/qy/7vc4/j6KY12FtdgUXznBj2+fHk0wDufehHmBCTQYjEEIiTm9die' + . '+WyWP3mq49o7/0mX1/c0winYItvdKg7bmmoQkNpccIMS3GwvgrHG2ux//ZTTAZDJnknE' + . 'caNS9IPlDOahOrc7o0aAPHStLIMl/dtzdmwU3sno0C0KCRJ155clJWBMR9aX3SjrasvS' + . 'X1ObK7DfLvdJO8kSil9sW7KnsqzpWj8MupF49X7GJ8JRNzx0AZcad6REP1t2FKxBB29g' + . '2aok5RFwJFSg5BdtLrp1ssejE9Nx9rvvvmcdOuy+QU5BTsdw5ZyZAK64/yMgYS2Ee9EU' + . 'p8Cu83YfTN6pxADIUYkWxQs0OmNI1LmOY30MQSCZJGepuqiN87InDmmxSkiNolITkyke' + . 'ZLEwJyEmMSECFO8k81qSeO50jBhik2EszBsPe9ES6HTMYsgRGJSnMBsghANgzh84wFc9' + . 'vgUvum4G606c03V1z0xlTSnkT65g4gCyVB+eMZ1276OeDKON9LnnzPB72lHFkzwDSLvm' + . 'ZA3b1KenwDOqdOcOpmtTvnOROwlC9dMqBJ8ISmbCM4MAZYVXIOY8g0q+215z23VpEMiP' + . 'jy7wHYUEBzqRIUXmfGPoqfzIYNCJQjNy3gn/Vkgn2gBlVSWU2EHRQWIvOPmobBDVvbal' + . '226vlNhJ3W/BE06xM5TWAb/O5rfcwiCMeClMqqsNSho1CmoNEDpzAA4wM/XOKKyxilln' + . 'eyziMBfAQYA4zp6M1LIbMYAAAAASUVORK5CYII='; + } + + /** + * {@inheritdoc} + */ + public static function create(UrlGenerator $generator, SessionInterface $session, array $options) + { + if (!isset($options['client-id'])) { + throw new InvalidArgumentException('Missing LinkedIn client-id parameter'); + } + + if (!isset($options['client-secret'])) { + throw new InvalidArgumentException('Missing LinkedIn client-secret parameter'); + } + + return new Linkedin($generator, $session, new Guzzle(), $options['client-id'], $options['client-secret']); + } +} diff --git a/lib/Alchemy/Phrasea/Authentication/Provider/ProviderInterface.php b/lib/Alchemy/Phrasea/Authentication/Provider/ProviderInterface.php new file mode 100644 index 0000000000..5043663b79 --- /dev/null +++ b/lib/Alchemy/Phrasea/Authentication/Provider/ProviderInterface.php @@ -0,0 +1,88 @@ + Twitter + * google-plus => GooglePlus + * + * @return string + */ + public function getId(); + + /** + * Returns an UTF-8 name for the provider. + * + * @return string + */ + public function getName(); + + /** + * Redirects to the actual authentication provider + * + * @return RedirectResponse + */ + public function authenticate(); + + + /** + * This method is called on provider callback, whenever the auth was + * successful or failure. + * + * @param Application $app + * @param Request $request + */ + public function onCallback(Request $request); + + /** + * Returns the identity + * + * @return Identity + */ + public function getIdentity(); + + /** + * Returns a Token + * + * @return Token + */ + public function getToken(); + + /** + * Get an URI representing the provider + * + * @return string + */ + public function getIconURI(); + + /** + * Creates a provider + * + * @param UrlGenerator $generator + * @param SessionInterface $session + * @param array $options + */ + public static function create(UrlGenerator $generator, SessionInterface $session, array $options); +} diff --git a/lib/Alchemy/Phrasea/Authentication/Provider/Twitter.php b/lib/Alchemy/Phrasea/Authentication/Provider/Twitter.php new file mode 100644 index 0000000000..7a8cedd4ef --- /dev/null +++ b/lib/Alchemy/Phrasea/Authentication/Provider/Twitter.php @@ -0,0 +1,244 @@ +generator = $generator; + $this->twitter = $twitter; + $this->session = $session; + } + + /** + * @param \tmhOAuth $twitter + * + * @return Twitter + */ + public function setTwitterClient(\tmhOAuth $twitter) + { + $this->twitter = $twitter; + + return $this; + } + + /** + * @return \tmhOAuth + */ + public function getTwitterClient() + { + return $this->twitter; + } + + /** + * {@inheritdoc} + */ + public function getId() + { + return 'twitter'; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'Twitter'; + } + + /** + * {@inheritdoc} + */ + public function authenticate() + { + $code = $this->twitter->request( + 'POST', + $this->twitter->url('oauth/request_token', ''), + array('oauth_callback' => $this->generator->generate( + 'login_authentication_provider_callback', + array('providerId' => $this->getId()), + UrlGenerator::ABSOLUTE_URL + )) + ); + + if ($code != 200) { + throw new RuntimeException('Unable to request twitter token'); + } + + $oauth = $this->twitter->extract_params($this->twitter->response['response']); + + $this->session->set('twitter.provider.oauth', $oauth); + + return new RedirectResponse(sprintf( + '%s?%s', + $this->twitter->url("oauth/authenticate", ''), + http_build_query(array('oauth_token' => $oauth['oauth_token']), '', '&') + )); + } + + /** + * {@inheritdoc} + */ + public function onCallback(Request $request) + { + $oauth = $this->session->get('twitter.provider.oauth'); + + $this->twitter->config['user_token'] = $oauth['oauth_token']; + $this->twitter->config['user_secret'] = $oauth['oauth_token_secret']; + + $code = $this->twitter->request( + 'POST', + $this->twitter->url('oauth/access_token', ''), + array('oauth_verifier' => $request->query->get('oauth_verifier')) + ); + + if ($code != 200) { + throw new RuntimeException('Unable to get twitter access token'); + } + + $access_token = $this->twitter->extract_params($this->twitter->response['response']); + $this->session->set('twitter.provider.access_token', $access_token); + $this->session->remove('twitter.provider.oauth'); + + $this->twitter->config['user_token'] = $access_token['oauth_token']; + $this->twitter->config['user_secret'] = $access_token['oauth_token_secret']; + + $code = $this->twitter->request( + 'GET', $this->twitter->url('1/account/verify_credentials') + ); + + if ($code != 200) { + throw new RuntimeException('Unable to get twitter credentials'); + } + + $resp = json_decode($this->twitter->response['response'], true); + + $this->session->set('twitter.provider.id', $resp['id']); + } + + /** + * {@inheritdoc} + */ + public function getToken() + { + if (0 >= $this->session->get('twitter.provider.id')) { + throw new RuntimeException('Provider has not authenticated'); + } + + return new Token($this, $this->session->get('twitter.provider.id')); + } + + /** + * {@inheritdoc} + */ + public function getIdentity() + { + $access_token = $this->session->get('twitter.provider.access_token'); + + $this->twitter->config['user_token'] = $access_token['oauth_token']; + $this->twitter->config['user_secret'] = $access_token['oauth_token_secret']; + + $code = $this->twitter->request( + 'GET', $this->twitter->url('1/account/verify_credentials') + ); + + if ($code != 200) { + throw new RuntimeException('Unable to retrieve twitter identity'); + } + + $resp = json_decode($this->twitter->response['response'], true); + + $identity = new Identity(); + + $identity->set(Identity::PROPERTY_USERNAME, $resp['screen_name']); + $identity->set(Identity::PROPERTY_IMAGEURL, $resp['profile_image_url_https']); + $identity->set(Identity::PROPERTY_ID, $resp['id']); + + return $identity; + } + + /** + * {@inheritdoc} + */ + public function getIconURI() + { + return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADEAAAAwCAYAAAC4w' + . 'JK5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2hpVFh0WE1MO' + . 'mNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ' + . '2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6b' + . 'WV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgM' + . 'jAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJod' + . 'HRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZ' + . 'XNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZ' + . 'S5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL' + . '3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZ' + . 'G9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZ' + . 'DowMjgwMTE3NDA3MjA2ODExOEMxNEY3NEJCM0UzNjU4QyIgeG1wTU06RG9jdW1lbnRJR' + . 'D0ieG1wLmRpZDpFRjg3OUMwRTcxRUMxMUUyQjc5NzlGRUJDNjcwRkVDMyIgeG1wTU06S' + . 'W5zdGFuY2VJRD0ieG1wLmlpZDpFRjg3OUMwRDcxRUMxMUUyQjc5NzlGRUJDNjcwRkVDM' + . 'yIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M2IChNYWNpbnRvc2gpI' + . 'j4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MDI4M' + . 'DExNzQwNzIwNjgxMThDMTRGNzRCQjNFMzY1OEMiIHN0UmVmOmRvY3VtZW50SUQ9Inhtc' + . 'C5kaWQ6MDI4MDExNzQwNzIwNjgxMThDMTRGNzRCQjNFMzY1OEMiLz4gPC9yZGY6RGVzY' + . '3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiP' + . 'z67Nf6GAAAEqklEQVR42txaS2zcRBj+vXa8u26UV9NQQtsESFW1OVCVnuASDkXqpRISC' + . 'HGgl9IeUNRDxRFx44B64UaLVFU8hYRUcUC9IFW8GsRDREWKFEAJJTyySTbpZr3rzfoxw' + . 'z/j8darpM44D8nDSP/uOrY8/zf/983/z0w0Simo3nLwP2gG+9A0jX8JY8D02O8sNYLGq' + . 'BPEflND3OQOvzY5c7JZ2PtBw4XDWRxx4nszpZ+/u3Dz/Jmf8NJF89if4yA6Cv0DH95b9' + . 'keqdT+rzHm8MHLiGn6PodloDgNjxLRhUl0foeBDlqWeM/PD+PWQuGSj7UcgmAbyBL0Pk' + . 'GmUZF7LAyIKzJptdGIg2IxLsj/rdqNZzGfme5xOeih5iiAyj6LI6C8YlDPidCM8ClSFS' + . 'Bhi4HOtPHF/EqYQcEqplcXbQARCD4FilUgbCD8CQkD1SDBNEIVBCCBK04kIUVOisLA9B' + . 'OCh/74kBl2Tj9qTvSY8N2TBwT0GVD0C35TW4MZfDu/roGVAB75rdos12zo6MVHLBuL0Y' + . 'BH68zq8N1tLfO6FQxZcHO3mZUHUnujLwzMPF8HHzgYRxNlvl7acn9pnJ8ryBJrk2473m' + . 'XD6gAWFHIUrv9c2dOJAUYfxo11tAKI2itFhkfx8rgZn8D3vbzIYknQSQCRBGMKzlw93w' + . 'TEc2cu/rMJszWt7Zmx/AfSclkjJYz15eGe6LN1v4vKUxOgkY3dW3Puc35uHj8YG4M0Tv' + . 'fDUPhOd0/gzvfnkxWETQ3Hu9hKUm0S6Xwk6ERwRuTxx488avPhYJxdrNKqnHrG4udjbH' + . '1UPrI5kEBUUue0GO5vsfLGmkGmXRnvhru21QMSbiRQ60mNu+o7ltUC6P0lhCzpJvvTfu' + . 'gfPP9qzLQemK+62y5y2WHutjE2l7OOZGiytbY8KtzFfyPYXWbKwWd2EKKikVRoBnL21A' + . 'FMxgadpLOnd+tuR7i+y5LKDz0w4S6SIb2XNh3nHh6EuAzqNdNtU13+tQn2bot5YE0Fos' + . 'm2pHqA4SWoA/2CJcX1qNVVfUiAoCaNAU04Xb0wswkrTh1eOdkNHQmJraQ+n3/GvF8Bu7' + . 'Mz+VtvwUfHBK9kU5iHoyz+U4dnP5mC26iV2yKI9/tUi3Ck1UvcT2aalOHmAeNblAcxsO' + . 'czKnVh+jmC2PjXUCS9h+VE0HhyJElLo1S9LMDnf2MVFEQnrJiJBJ8PQ4dzxPrgw2sOBJ' + . 'LU6Dv+7UxW4NrkMdjOAnW7rMjZbEMlEwsYZ6e2JBbjyYxmeHt4DJ/dbWASasM/S+f0yT' + . 'r/TOPV+P+/AxFwdGrvg/MbCxumVizvFlo3j+vDFb6vcMrM8ZSmCBgovTymvm6jaIAKxS' + . 'UBV3rLhZ0eqR4JTiWtC4X0nEQq16cQ3z9Q4KUrKE1RQSmVNRAlPYToR6jTmUBeHIMMYS' + . 'H1lGcJTCCKsVYqzi6D66dXXSb06v9USebctsMv3nJtvfYK+sjLYjXZeNXZT07Q8XnRBe' + . 'D48jDaI1g/hCaWekSAwh9mRb5lttKDdRVtgS3Uj9kATwlP6xaiChvCU0sgICF9EYFX4a' + . 'AufAyNGJ1fcAIGYRcGE7PyTSuSjI/y0xTX5T4ABAP5AumSxg/sSAAAAAElFTkSuQmCC'; + } + + /** + * {@inheritdoc} + */ + public static function create(UrlGenerator $generator, SessionInterface $session, array $options) + { + $twitter = new \tmhOAuth(array( + 'consumer_key' => $options['consumer-key'], + 'consumer_secret' => $options['consumer-secret'], + )); + + return new Twitter($generator, $session, $twitter); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Authentication/Provider/FacebookTest.php b/tests/Alchemy/Tests/Phrasea/Authentication/Provider/FacebookTest.php new file mode 100644 index 0000000000..d488cd5c01 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Authentication/Provider/FacebookTest.php @@ -0,0 +1,146 @@ +markTestSkipped('testGetSetSession disabled for facebook'); + } + + public function testGetSetGuzzleClient() + { + $this->markTestSkipped('testGetSetGuzzleClient disabled for facebook'); + } + + public function testIsBuiltWithFactory() + { + $this->markTestSkipped('Skipping because \Facebook runs session_start'); + } + + public function testCreate() + { + $this->markTestSkipped('Skipping because \Facebook runs session_start'); + } + + public function provideDataForFailingCallback() + { + $provider = $this->getProvider(); + $provider->getFacebook()->expects($this->any()) + ->method('getUser') + ->will($this->returnValue(null)); + + return array( + array($provider, $this->getRequestMock()) + ); + } + + public function provideDataForSuccessCallback() + { + $provider = $this->getProvider(); + $provider->getFacebook()->expects($this->any()) + ->method('getUser') + ->will($this->returnValue('123456')); + + return array( + array($provider, $this->getRequestMock()) + ); + } + + protected function getProvider() + { + return new Facebook($this->getFacebookMock(), $this->getUrlGeneratorMock()); + } + + protected function authenticate(ProviderInterface $provider) + { + $provider->getFacebook()->expects($this->any()) + ->method('getUser') + ->will($this->returnValue('123456')); + } + + protected function getProviderForSuccessIdentity() + { + $provider = $this->getProvider(); + $this->authenticate($provider); + + $facebook = $this->getMockBuilder('Facebook') + ->disableOriginalConstructor() + ->setMethods(array('getLoginUrl', 'api', 'getUser')) + ->getMock(); + + $facebook->expects($this->any()) + ->method('getLoginUrl') + ->will($this->returnValue('http://www.facebook.com/')); + + $facebook->expects($this->once()) + ->method('api') + ->will($this->returnValue(array( + 'id' => self::ID, + 'username' => self::FIRSTNAME, + 'first_name' => self::FIRSTNAME, + 'last_name' => self::LASTNAME, + 'email' => self::EMAIL, + ))); + + $provider->setFacebook($facebook); + + return $provider; + } + + protected function getProviderForFailingIdentity() + { + return $this->getProvider(); + } + + protected function getAvailableFieldsForIdentity() + { + return array( + Identity::PROPERTY_ID => self::ID, + Identity::PROPERTY_USERNAME => self::FIRSTNAME, + Identity::PROPERTY_FIRSTNAME => self::FIRSTNAME, + Identity::PROPERTY_LASTNAME => self::LASTNAME, + Identity::PROPERTY_EMAIL => self::EMAIL, + ); + } + + protected function getTestOptions() + { + return array( + 'app-id' => 'zizi', + 'secret' => 's3cr3t', + ); + } + + protected function getProviderForAuthentication() + { + return $this->getProvider(); + } + + private function getFacebookMock() + { + $facebook = $this->getMockBuilder('Facebook') + ->disableOriginalConstructor() + ->setMethods(array('getLoginUrl', 'api', 'getUser')) + ->getMock(); + + $facebook->expects($this->any()) + ->method('getLoginUrl') + ->will($this->returnValue('http://www.facebook.com/')); + + $facebook->expects($this->any()) + ->method('api') + ->will($this->returnCallback(function () use ($facebook) { + if (!$facebook->getUser()) { + throw new \FacebookApiException('Not authenticated'); + } + })); + + return $facebook; + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Authentication/Provider/GithubTest.php b/tests/Alchemy/Tests/Phrasea/Authentication/Provider/GithubTest.php new file mode 100644 index 0000000000..48a64df5f2 --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Authentication/Provider/GithubTest.php @@ -0,0 +1,204 @@ +provideFailingProvider(); + $provider->getSession()->set('github.provider.state', $state . mt_rand()); + + $request = $this->getRequestMock(); + $this->addQueryParameter($request, array('state' => $state)); + + return array( + array($this->getProvider(), $this->getRequestMock()), + array($provider, $request), + ); + } + + public function provideDataForSuccessCallback() + { + $provider = $this->getProvider(); + + $guzzle = $this->getMock('Guzzle\Http\ClientInterface'); + + $requestGet = $this->getMock('Guzzle\Http\Message\RequestInterface'); + $requestPost = $this->getMock('Guzzle\Http\Message\EntityEnclosingRequestInterface'); + + $queryString = $this->getMockBuilder('Guzzle\Http\QueryString') + ->disableOriginalConstructor() + ->getMock(); + + $requestGet->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($queryString)); + + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->disableOriginalConstructor() + ->getMock(); + + $response->expects($this->any()) + ->method('getStatusCode') + ->will($this->returnValue(200)); + + $requestGet->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $requestPost->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $guzzle->expects($this->any()) + ->method('get') + ->will($this->returnValue($requestGet)); + + $guzzle->expects($this->any()) + ->method('post') + ->will($this->returnValue($requestPost)); + + $provider->setGuzzleClient($guzzle); + + $state = md5(mt_rand()); + $provider->getSession()->set('github.provider.state', $state); + + $request = $this->getRequestMock(); + $this->addQueryParameter($request, array('state' => $state)); + + return array( + array($provider, $request), + ); + } + + public function getTestOptions() + { + return array( + 'client-id' => 'github-client-id', + 'client-secret' => 'github-client-secret', + ); + } + + protected function getProviderForAuthentication() + { + return $this->getProvider(); + } + + protected function getProviderForSuccessIdentity() + { + $provider = $this->getProvider(); + + $guzzle = $this->getMock('Guzzle\Http\ClientInterface'); + + $requestGet = $this->getMock('Guzzle\Http\Message\RequestInterface'); + + $queryString = $this->getMockBuilder('Guzzle\Http\QueryString') + ->disableOriginalConstructor() + ->getMock(); + + $requestGet->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($queryString)); + + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->disableOriginalConstructor() + ->getMock(); + + $response->expects($this->any()) + ->method('getStatusCode') + ->will($this->returnValue(200)); + + $response->expects($this->any()) + ->method('getBody') + ->with($this->equalTo(true)) + ->will($this->returnValue(json_encode(array( + 'id' => self::ID, + 'name' => self::FIRSTNAME . ' ' . self::LASTNAME, + 'email' => self::EMAIL, + 'avatar_url' => self::IMAGEURL, + )))); + + $requestGet->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $guzzle->expects($this->any()) + ->method('get') + ->will($this->returnValue($requestGet)); + + $provider->setGuzzleClient($guzzle); + $provider->getSession()->set('github.provider.id', 'github-id'); + + return $provider; + } + + protected function getProviderForFailingIdentity() + { + return $this->provideFailingProvider(); + } + + protected function provideFailingProvider() + { + $provider = $this->getProvider(); + + $guzzle = $this->getMock('Guzzle\Http\ClientInterface'); + + $requestGet = $this->getMock('Guzzle\Http\Message\RequestInterface'); + + $queryString = $this->getMockBuilder('Guzzle\Http\QueryString') + ->disableOriginalConstructor() + ->getMock(); + + $requestGet->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($queryString)); + + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->disableOriginalConstructor() + ->getMock(); + + $response->expects($this->any()) + ->method('getStatusCode') + ->will($this->returnValue(401)); + + $requestGet->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $guzzle->expects($this->any()) + ->method('get') + ->will($this->returnValue($requestGet)); + + $provider->setGuzzleClient($guzzle); + + return $provider; + } + + protected function getAvailableFieldsForIdentity() + { + return array( + Identity::PROPERTY_ID => self::ID, + Identity::PROPERTY_FIRSTNAME => self::FIRSTNAME, + Identity::PROPERTY_LASTNAME => self::LASTNAME, + Identity::PROPERTY_EMAIL => self::EMAIL, + Identity::PROPERTY_IMAGEURL => self::IMAGEURL, + ); + } + + protected function authenticate(ProviderInterface $provider) + { + $provider->getSession()->set('github.provider.id', 'github-id'); + } + + protected function getProvider() + { + return new Github($this->getUrlGeneratorMock(), $this->getMockSession(), $this->getGuzzleMock(), 'key', 'secret'); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Authentication/Provider/GooglePlusTest.php b/tests/Alchemy/Tests/Phrasea/Authentication/Provider/GooglePlusTest.php new file mode 100644 index 0000000000..5dffcbb91a --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Authentication/Provider/GooglePlusTest.php @@ -0,0 +1,258 @@ +getProvider(); + $this->assertInstanceOf('Google_client', $provider->getGoogleClient()); + $google = $this->getMockBuilder('Google_Client') + ->disableOriginalConstructor() + ->getMock(); + + $provider->setGoogleClient($google); + $this->assertEquals($google, $provider->getGoogleClient()); + } + + public function provideDataForFailingCallback() + { + $provider = $this->getProvider(); + + $state = md5(mt_rand()); + $provider->getSession()->set('google-plus.provider.state', $state . mt_rand()); + + $request = $this->getRequestMock(); + $this->addQueryParameter($request, array('state' => $state)); + + return array( + array($this->provideFailingProvider(), $this->getRequestMock()), + array($provider, $request), + ); + } + + public function provideDataForSuccessCallback() + { + $provider = $this->getProvider(); + + $guzzle = $this->getMock('Guzzle\Http\ClientInterface'); + + $requestGet = $this->getMock('Guzzle\Http\Message\RequestInterface'); + $requestPost = $this->getMock('Guzzle\Http\Message\EntityEnclosingRequestInterface'); + + $queryString = $this->getMockBuilder('Guzzle\Http\QueryString') + ->disableOriginalConstructor() + ->getMock(); + + $requestGet->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($queryString)); + + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->disableOriginalConstructor() + ->getMock(); + + $response->expects($this->any()) + ->method('getStatusCode') + ->will($this->returnValue(200)); + + $requestGet->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $requestPost->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $guzzle->expects($this->any()) + ->method('get') + ->will($this->returnValue($requestGet)); + + $guzzle->expects($this->any()) + ->method('post') + ->will($this->returnValue($requestPost)); + + $provider->setGuzzleClient($guzzle); + + $state = md5(mt_rand()); + $provider->getSession()->set('google-plus.provider.state', $state); + + $request = $this->getRequestMock(); + $this->addQueryParameter($request, array( + 'state' => $state, + )); + + $ticket = $this->getMockBuilder('Google_LoginTicket') + ->disableOriginalConstructor() + ->getMock(); + + $provider->getGoogleClient()->expects($this->once()) + ->method('verifyIdToken') + ->will($this->returnValue($ticket)); + + return array( + array($provider, $request), + ); + } + + public function getTestOptions() + { + return array( + 'client-id' => 'google-plus-client-id', + 'client-secret' => 'google-plus-client-secret', + ); + } + + protected function getProviderForSuccessIdentity() + { + $provider = $this->getProvider(); + + $guzzle = $this->getMock('Guzzle\Http\ClientInterface'); + + $requestGet = $this->getMock('Guzzle\Http\Message\RequestInterface'); + + $queryString = $this->getMockBuilder('Guzzle\Http\QueryString') + ->disableOriginalConstructor() + ->getMock(); + + $requestGet->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($queryString)); + + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->disableOriginalConstructor() + ->getMock(); + + $response->expects($this->any()) + ->method('getStatusCode') + ->will($this->returnValue(200)); + + $response->expects($this->any()) + ->method('getBody') + ->with($this->equalTo(true)) + ->will($this->returnValue(json_encode(array( + 'email' => self::EMAIL + )))); + + $requestGet->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $guzzle->expects($this->any()) + ->method('get') + ->will($this->returnValue($requestGet)); + + $provider->setGuzzleClient($guzzle); + $provider->getSession()->set('google-plus.provider.id', '12345678'); + + $people = $this->getMockBuilder('Google_PeopleServiceResource') + ->disableOriginalConstructor() + ->getMock(); + + $people->expects($this->once()) + ->method('get') + ->will($this->returnValue(array( + 'name' => array( + 'givenName' => self::FIRSTNAME, + 'familyName' => self::LASTNAME, + ), + 'id' => self::ID, + 'image' => array( + 'url' => self::IMAGEURL + ) + ))); + + $provider->getGooglePlusService()->people = $people; + + return $provider; + } + + protected function getAvailableFieldsForIdentity() + { + return array( + Identity::PROPERTY_ID => self::ID, + Identity::PROPERTY_FIRSTNAME => self::FIRSTNAME, + Identity::PROPERTY_LASTNAME => self::LASTNAME, + Identity::PROPERTY_EMAIL => self::EMAIL, + Identity::PROPERTY_IMAGEURL => self::IMAGEURL, + ); + } + + protected function getProviderForFailingIdentity() + { + return $this->provideFailingProvider(); + } + + protected function provideFailingProvider() + { + $provider = $this->getProvider(); + + $guzzle = $this->getMock('Guzzle\Http\ClientInterface'); + + $requestGet = $this->getMock('Guzzle\Http\Message\RequestInterface'); + + $queryString = $this->getMockBuilder('Guzzle\Http\QueryString') + ->disableOriginalConstructor() + ->getMock(); + + $requestGet->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($queryString)); + + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->disableOriginalConstructor() + ->getMock(); + + $response->expects($this->any()) + ->method('getStatusCode') + ->will($this->returnValue(401)); + + $requestGet->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $guzzle->expects($this->any()) + ->method('get') + ->will($this->returnValue($requestGet)); + + $provider->setGuzzleClient($guzzle); + + return $provider; + } + + protected function authenticate(ProviderInterface $provider) + { + $provider->getSession()->set('google-plus.provider.id', '12345678'); + } + + protected function getProvider() + { + $googleMock = $this->getMockBuilder('Google_Client') + ->disableOriginalConstructor() + ->getMock(); + + $googleMock->expects($this->any()) + ->method('createAuthUrl') + ->will($this->returnValue('https://www.google.com/auth')); + + $plus = $this->getMockBuilder('Google_PlusService') + ->disableOriginalConstructor() + ->getMock(); + + + $google = new GooglePlus($this->getUrlGeneratorMock(), $this->getMockSession(), $googleMock, $this->getGuzzleMock()); + $google->setGooglePlusService($plus); + + return $google; + } + + protected function getProviderForAuthentication() + { + return $this->getProvider(); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Authentication/Provider/LinkedinTest.php b/tests/Alchemy/Tests/Phrasea/Authentication/Provider/LinkedinTest.php new file mode 100644 index 0000000000..a69857c8fd --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Authentication/Provider/LinkedinTest.php @@ -0,0 +1,280 @@ +getRequestMock(); + $this->addQueryParameter($request, array('state' => $state)); + + $provider1 = $this->getProvider(); + $provider1->setGuzzleClient($this->getGuzzleMock(401)); + $provider1->getSession()->set('linkedin.provider.state', $state); + + $mock = $this->getMock('Guzzle\Http\ClientInterface'); + + $requestGet = $this->getMock('Guzzle\Http\Message\RequestInterface'); + $requestPost = $this->getMock('Guzzle\Http\Message\RequestInterface'); + + $queryString = $this->getMockBuilder('Guzzle\Http\QueryString') + ->disableOriginalConstructor() + ->getMock(); + $queryString->expects($this->any()) + ->method('add') + ->will($this->returnSelf()); + + $requestGet->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($queryString)); + + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->disableOriginalConstructor() + ->getMock(); + + $response->expects($this->at(0)) + ->method('getStatusCode') + ->will($this->returnValue(200)); + + $response->expects($this->at(1)) + ->method('getStatusCode') + ->will($this->returnValue(401)); + + $requestGet->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $requestPost->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $mock->expects($this->any()) + ->method('get') + ->will($this->returnValue($requestGet)); + + $mock->expects($this->any()) + ->method('post') + ->will($this->returnValue($requestPost)); + + + $provider2 = $this->getProvider(); + $provider2->setGuzzleClient($mock); + $provider2->getSession()->set('linkedin.provider.state', $state); + + + return array( + array($this->getProvider(), $this->getRequestMock()), + array($provider1, $request), + array($provider2, $request), + ); + } + + public function provideDataForSuccessCallback() + { + $provider = $this->getProvider(); + + $guzzle = $this->getMock('Guzzle\Http\ClientInterface'); + + $requestGet = $this->getMock('Guzzle\Http\Message\RequestInterface'); + $requestPost = $this->getMock('Guzzle\Http\Message\EntityEnclosingRequestInterface'); + + $queryString = $this->getMockBuilder('Guzzle\Http\QueryString') + ->disableOriginalConstructor() + ->getMock(); + + $queryString->expects($this->any()) + ->method('add') + ->will($this->returnSelf()); + + $requestGet->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($queryString)); + + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->disableOriginalConstructor() + ->getMock(); + + $response->expects($this->any()) + ->method('getStatusCode') + ->will($this->returnValue(200)); + + $requestGet->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $requestPost->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $guzzle->expects($this->any()) + ->method('get') + ->will($this->returnValue($requestGet)); + + $guzzle->expects($this->any()) + ->method('post') + ->will($this->returnValue($requestPost)); + + $provider->setGuzzleClient($guzzle); + + $state = md5(mt_rand()); + $provider->getSession()->set('linkedin.provider.state', $state); + + $request = $this->getRequestMock(); + $this->addQueryParameter($request, array('state' => $state)); + + return array( + array($provider, $request), + ); + } + + public function getTestOptions() + { + return array( + 'client-id' => 'linkedin-client-id', + 'client-secret' => 'linkedin-client-secret', + ); + } + + protected function getProviderForSuccessIdentity() + { + $provider = $this->getProvider(); + + $guzzle = $this->getMock('Guzzle\Http\ClientInterface'); + + $requestGet = $this->getMock('Guzzle\Http\Message\RequestInterface'); + + $queryString = $this->getMockBuilder('Guzzle\Http\QueryString') + ->disableOriginalConstructor() + ->getMock(); + + $queryString->expects($this->any()) + ->method('add') + ->will($this->returnSelf()); + + $requestGet->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($queryString)); + + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->disableOriginalConstructor() + ->getMock(); + + $response->expects($this->any()) + ->method('getStatusCode') + ->will($this->returnValue(200)); + + $response->expects($this->any()) + ->method('getBody') + ->with($this->equalTo(true)) + ->will($this->returnValue(json_encode(array( + 'positions' => array( + '_total' => 1, + 'values' => array( + array( + 'company' => array( + 'name' => self::COMPANY + ) + ) + ) + ), + 'emailAddress' => self::EMAIL, + 'firstName' => self::FIRSTNAME, + 'id' => self::ID, + 'pictureUrl' => self::IMAGEURL, + 'lastName' => self::LASTNAME, + )))); + + $requestGet->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $guzzle->expects($this->any()) + ->method('get') + ->will($this->returnValue($requestGet)); + + $provider->setGuzzleClient($guzzle); + $provider->getSession()->set('linkedin.provider.id', 'linkedin-id'); + + return $provider; + } + + protected function getAvailableFieldsForIdentity() + { + return array( + Identity::PROPERTY_ID => self::ID, + Identity::PROPERTY_FIRSTNAME => self::FIRSTNAME, + Identity::PROPERTY_LASTNAME => self::LASTNAME, + Identity::PROPERTY_EMAIL => self::EMAIL, + Identity::PROPERTY_IMAGEURL => self::IMAGEURL, + Identity::PROPERTY_COMPANY => self::COMPANY, + ); + } + + protected function getProviderForFailingIdentity() + { + return $this->provideFailingProvider(); + } + + protected function provideFailingProvider() + { + $provider = $this->getProvider(); + + $guzzle = $this->getMock('Guzzle\Http\ClientInterface'); + + $requestGet = $this->getMock('Guzzle\Http\Message\RequestInterface'); + + $queryString = $this->getMockBuilder('Guzzle\Http\QueryString') + ->disableOriginalConstructor() + ->getMock(); + + $queryString->expects($this->any()) + ->method('add') + ->will($this->returnSelf()); + + $requestGet->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($queryString)); + + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->disableOriginalConstructor() + ->getMock(); + + $response->expects($this->any()) + ->method('getStatusCode') + ->will($this->returnValue(401)); + + $requestGet->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $guzzle->expects($this->any()) + ->method('get') + ->will($this->returnValue($requestGet)); + + $provider->setGuzzleClient($guzzle); + + return $provider; + } + + protected function getProviderForAuthentication() + { + return $this->getProvider(); + } + + protected function authenticate(ProviderInterface $provider) + { + $provider->getSession()->set('linkedin.provider.id', 'linkedin-id'); + } + + protected function getProvider() + { + return new Linkedin($this->getUrlGeneratorMock(), $this->getMockSession(), $this->getGuzzleMock(), 'key', 'secret'); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Authentication/Provider/ProviderTestCase.php b/tests/Alchemy/Tests/Phrasea/Authentication/Provider/ProviderTestCase.php new file mode 100644 index 0000000000..37cde2e0cf --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Authentication/Provider/ProviderTestCase.php @@ -0,0 +1,262 @@ +assertInternalType('string', $this->getProvider()->getId()); + } + + public function testGetSetUrlGenerator() + { + $provider = $this->getProvider(); + $this->assertInstanceOf('Symfony\Component\Routing\Generator\UrlGenerator', $provider->getUrlGenerator()); + $generator = $this->getUrlGeneratorMock(); + + $provider->setUrlGenerator($generator); + $this->assertEquals($generator, $provider->getUrlGenerator()); + } + + public function testGetSetSession() + { + $provider = $this->getProvider(); + $this->assertInstanceOf('Symfony\Component\HttpFoundation\Session\SessionInterface', $provider->getSession()); + $session = $this->getMockSession(); + + $provider->setSession($session); + $this->assertEquals($session, $provider->getSession()); + } + + public function testGetSetGuzzleClient() + { + $provider = $this->getProvider(); + $this->assertInstanceOf('Guzzle\Http\CLientInterface', $provider->getGuzzleClient()); + $guzzle = $this->getGuzzleMock(); + + $provider->setGuzzleClient($guzzle); + $this->assertEquals($guzzle, $provider->getGuzzleClient()); + } + + public function testGetName() + { + $this->assertInternalType('string', $this->getProvider()->getName()); + } + + public function testIsBuiltWithFactory() + { + $provider = $this->getProvider(); + + $built = $this->getProviderFactory()->build($provider->getId(), $this->getTestOptions()); + + $this->assertInstanceOf(get_class($provider), $built); + } + + public function testAuthenticate() + { + $provider = $this->getProviderForAuthentication(); + $redirect = $provider->authenticate(); + + $this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $redirect); + } + + /** + * @dataProvider provideDataForSuccessCallback + */ + public function testOnCallbackWithSuccess(ProviderInterface $provider, $request) + { + $provider->onCallback($request); + } + + /** + * @dataProvider provideDataForFailingCallback + * @expectedException Alchemy\Phrasea\Exception\RuntimeException + */ + public function testOnCallbackWithFailure($provider, $request) + { + $provider->onCallback($request); + } + + public function testGetToken() + { + $provider = $this->getProvider(); + $this->authenticate($provider); + + $token = $provider->getToken(); + + $this->assertInstanceOf('Alchemy\Phrasea\Authentication\Provider\Token\Token', $token); + $this->assertEquals($provider, $token->getProvider()); + } + + /** + * @expectedException Alchemy\Phrasea\Exception\RuntimeException + */ + public function testGetTokenWhenNotAuthenticated() + { + $this->getProvider()->getToken(); + } + + public function testGetIdentity() + { + $provider = $this->getProviderForSuccessIdentity(); + $identity = $provider->getIdentity(); + + $this->assertInstanceOf('Alchemy\Phrasea\Authentication\Provider\Token\Identity', $identity); + + foreach ($this->getAvailableFieldsForIdentity() as $name=>$value) { + $this->assertEquals($value, $identity->get($name), "testing $name"); + } + } + + /** + * @expectedException Alchemy\Phrasea\Exception\RuntimeException + */ + public function testGetIdentityWhenNotAuthenticated() + { + $provider = $this->getProviderForFailingIdentity(); + $provider->getIdentity(); + } + + public function testGetIconURI() + { + Parser::parse($this->getProvider()->getIconURI()); + } + + public function testCreate() + { + $name = get_class($this->getProvider()); + $provider = $name::create($this->getUrlGeneratorMock(), $this->getMockSession(), $this->getTestOptions()); + + $this->assertInstanceOf($name, $provider); + } + + abstract public function provideDataForFailingCallback(); + + abstract public function provideDataForSuccessCallback(); + + protected function addQueryParameter(Request $request, array $parameters) + { + $query = $this->getMockBuilder('Symfony\Component\HttpFoundation\ParameterBag') + ->disableOriginalConstructor() + ->getMock(); + + $query->expects($this->any()) + ->method('get') + ->will($this->returnCallback(function ($param) use ($parameters) { + if (isset($parameters[$param])) { + return $parameters[$param]; + } + })); + + $request->query = $query; + } + + protected function getMockSession() + { + return new Session(new MockFileSessionStorage()); + } + + abstract protected function authenticate(ProviderInterface $provider); + + abstract protected function getProviderForAuthentication(); + + abstract protected function getProviderForSuccessIdentity(); + + abstract protected function getProviderForFailingIdentity(); + + abstract protected function getAvailableFieldsForIdentity(); + + /** + * @return ProviderInterface + */ + abstract protected function getProvider(); + + /** + * @return array + */ + abstract protected function getTestOptions(); + + protected function getUrlGeneratorMock() + { + return $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGenerator') + ->disableOriginalConstructor() + ->getMock(); + } + + protected function getRequestMock() + { + return $this->getMockBuilder('Symfony\Component\HttpFoundation\Request') + ->disableOriginalConstructor() + ->getMock(); + } + + protected function getGuzzleMock($statusCode = 200) + { + $mock = $this->getMock('Guzzle\Http\ClientInterface'); + + $requestGet = $this->getMock('Guzzle\Http\Message\RequestInterface'); + $requestPost = $this->getMock('Guzzle\Http\Message\RequestInterface'); + + $queryString = $this->getMockBuilder('Guzzle\Http\QueryString') + ->disableOriginalConstructor() + ->getMock(); + + $requestGet->expects($this->any()) + ->method('getQuery') + ->will($this->returnValue($queryString)); + + $response = $this->getMockBuilder('Guzzle\Http\Message\Response') + ->disableOriginalConstructor() + ->getMock(); + + $response->expects($this->any()) + ->method('getStatusCode') + ->will($this->returnValue($statusCode)); + + $requestGet->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $requestPost->expects($this->any()) + ->method('send') + ->will($this->returnValue($response)); + + $mock->expects($this->any()) + ->method('get') + ->will($this->returnValue($requestGet)); + + $mock->expects($this->any()) + ->method('post') + ->will($this->returnValue($requestPost)); + + return $mock; + } + + /** + * @return ProviderFactory + */ + private function getProviderFactory() + { + return new ProviderFactory($this->getUrlGeneratorMock(), $this->getMockSession()); + } +} diff --git a/tests/Alchemy/Tests/Phrasea/Authentication/Provider/TwitterTest.php b/tests/Alchemy/Tests/Phrasea/Authentication/Provider/TwitterTest.php new file mode 100644 index 0000000000..c35a0f224b --- /dev/null +++ b/tests/Alchemy/Tests/Phrasea/Authentication/Provider/TwitterTest.php @@ -0,0 +1,214 @@ +markTestSkipped('testGetSetGuzzleClient disabled for facebook'); + } + + public function testGetSetTwitterClient() + { + $provider = $this->getProvider(); + $this->assertInstanceOf('tmhOAuth', $provider->getTwitterClient()); + $client = $this->getMockBuilder('tmhOAuth') + ->disableOriginalConstructor() + ->getMock(); + + $provider->setTwitterClient($client); + $this->assertEquals($client, $provider->getTwitterClient()); + } + + /** + * @expectedException Alchemy\Phrasea\Exception\RuntimeException + */ + public function testAuthenticateWithFailure() + { + $provider = $this->getProvider(); + + $provider->getTwitterClient()->expects($this->once()) + ->method('request') + ->will($this->returnValue(401)); + + $provider->authenticate(); + } + + public function getProviderForAuthentication() + { + $provider = $this->getProvider(); + + $provider->getTwitterClient()->expects($this->once()) + ->method('request') + ->will($this->returncallback(function () use ($provider) { + $provider->getTwitterClient()->response = array( + 'response' => array( + 'oauth_token' => 'twitter-oauth-token', + ) + ); + + return 200; + })); + + return $provider; + } + + public function provideDataForFailingCallback() + { + $request = $this->getRequestMock(); + $this->addQueryParameter($request, array()); + + $provider1 = $this->getProvider(); + $provider1->getTwitterClient()->expects($this->once()) + ->method('request') + ->will($this->returnValue(401)); + + $first = true; + $provider2 = $this->getProvider(); + $provider2->getTwitterClient()->expects($this->exactly(2)) + ->method('request') + ->will($this->returnCallback(function () use (&$first, $provider2) { + if (!$first) { + return 401; + } else { + + $provider2->getTwitterClient()->response = array( + 'response' => array( + 'oauth_token' => 'twitter-oauth-token', + ) + ); + + $first = false; + return 200; + } + })); + + return array( + array($provider1, $request), + array($provider2, $request), + ); + } + + public function provideDataForSuccessCallback() + { + $provider = $this->getProvider(); + + $state = md5(mt_rand()); + $provider->getSession()->set('twitter.provider.state', $state); + + $request = $this->getRequestMock(); + $this->addQueryParameter($request, array('state' => $state)); + + $provider->getTwitterClient()->expects($this->any()) + ->method('request') + ->will($this->returnCallback(function ($method) use ($provider) { + switch($method) { + case 'POST': + $provider->getTwitterClient()->response = array( + 'response' => array( + 'oauth_token' => 'twitter-oauth-token', + ) + ); + break; + case 'GET': + $provider->getTwitterClient()->response = array( + 'response' => json_encode(array( + 'id' => self::ID, + )) + ); + break; + default: + throw new \InvalidArgumentException(sprintf('Invalid method %s', $method)); + break; + } + + return 200; + })); + + $provider->getTwitterClient()->expects($this->once()) + ->method('extract_params') + ->will($this->returnValue(array( + 'oauth_token_secret' => 'token secret', + 'oauth_token' => 'token', + ))); + + return array( + array($provider, $request), + ); + } + + public function getTestOptions() + { + return array( + 'consumer-key' => 'twitter-consumer-key', + 'consumer-secret' => 'twitter-consumer-secret', + ); + } + + protected function getProviderForSuccessIdentity() + { + $provider = $this->getProvider(); + $provider->getSession()->set('twitter.provider.access_token', array( + 'oauth_token' => 'twitter token', + 'oauth_token_secret' => 'token secret', + )); + + $provider->getTwitterClient()->expects($this->once()) + ->method('request') + ->will($this->returncallback(function () use ($provider) { + $provider->getTwitterClient()->response = array( + 'response' => json_encode(array( + 'screen_name' => self::USERNAME, + 'profile_image_url_https' => self::IMAGEURL, + 'id' => self::ID, + )) + ); + + return 200; + })); + + return $provider; + } + + protected function getAvailableFieldsForIdentity() + { + return array( + Identity::PROPERTY_ID => self::ID, + Identity::PROPERTY_USERNAME => self::USERNAME, + Identity::PROPERTY_IMAGEURL => self::IMAGEURL, + ); + } + + protected function getProviderForFailingIdentity() + { + return $this->provideFailingProvider(); + } + + protected function provideFailingProvider() + { + $provider = $this->getProvider(); + + return $provider; + } + + protected function authenticate(ProviderInterface $provider) + { + $provider->getSession()->set('twitter.provider.id', '12345'); + } + + protected function getProvider() + { + $twitter = $this->getMockBuilder('tmhOAuth') + ->disableOriginalConstructor() + ->getMock(); + + $twitter->config = $this->getTestOptions(); + + return new Twitter($this->getUrlGeneratorMock(), $this->getMockSession(), $twitter); + } +}