diff --git a/composer.json b/composer.json index 0257cf95..c61b59d3 100644 --- a/composer.json +++ b/composer.json @@ -1,11 +1,12 @@ { - "name": "lncd/Oauth2", + "name": "lncd/oauth2", "description": "OAuth 2.0 Framework", "version": "0.2.3", "homepage": "https://github.com/lncd/OAuth2", "license": "MIT", "require": { - "php": ">=5.3.0" + "php": ">=5.3.0", + "guzzle/guzzle": "*" }, "require-dev": { "phpunit/phpunit": "*" @@ -27,7 +28,7 @@ "authors": [ { "name": "Alex Bilbie", - "email": "oauth2@alexbilbie.com", + "email": "hello@alexbilbie.com", "homepage": "http://www.httpster.org", "role": "Developer" } @@ -36,5 +37,8 @@ "psr-0": { "Oauth2": "src/" } + }, + "suggest": { + "lncd/oauth2-facebook": "Adds support for Facebook as an IDP" } } \ No newline at end of file diff --git a/src/Oauth2/Client/IDP.php b/src/Oauth2/Client/IDP.php new file mode 100644 index 00000000..2dbe182b --- /dev/null +++ b/src/Oauth2/Client/IDP.php @@ -0,0 +1,230 @@ +result = $result; + + $code = isset($result['code']) ? $result['code'] : 0; + + if (isset($result['error'])) { + + // OAuth 2.0 Draft 10 style + $message = $result['error']; + + } elseif (isset($result['message'])) { + + // cURL style + $message = $result['message']; + + } else { + + $message = 'Unknown Error.'; + + } + + parent::__construct($message['message'], $message['code']); + } + + public function getType() + { + if (isset($this->result['error'])) { + + $message = $this->result['error']; + + if (is_string($message)) { + // OAuth 2.0 Draft 10 style + return $message; + } + } + + return 'Exception'; + } + + /** + * To make debugging easier. + * + * @returns + * The string representation of the error. + */ + public function __toString() + { + $str = $this->getType() . ': '; + + if ($this->code != 0) { + $str .= $this->code . ': '; + } + + return $str . $this->message; + } + +} + +abstract class IDP { + + public $clientId = ''; + + public $clientSecret = ''; + + public $redirectUri = ''; + + public $name; + + public $uidKey = 'uid'; + + public $scopes = array(); + + public $method = 'post'; + + public $scopeSeperator = ','; + + public $responseType = 'json'; + + public function __construct($options) + { + foreach ($options as $option => $value) { + if (isset($this->{$option})) { + $this->{$option} = $value; + } + } + } + + abstract public function urlAuthorize(); + + abstract public function urlAccessToken(); + + abstract public function urlUserDetails(\Oauth2\Client\Token\Access $token); + + abstract public function userDetails($response, \Oauth2\Client\Token\Access $token); + + public function authorize($options = array()) + { + $state = md5(uniqid(rand(), TRUE)); + setcookie($this->name.'_authorize_state', $state); + + $params = array( + 'client_id' => $this->clientId, + 'redirect_uri' => $this->redirectUri, + 'state' => $state, + 'scope' => is_array($this->scope) ? implode($this->scopeSeperator, $this->scope) : $this->scope, + 'response_type' => isset($options['response_type']) ? $options['response_type'] : 'code', + 'approval_prompt' => 'force' // - google force-recheck + ); + + header('Location: ' . $this->urlAuthorize().'?'.http_build_query($params)); + exit; + } + + public function getAccessToken($code = NULL, $options = array()) + { + if ($code === NULL) { + throw new \BadMethodCallException('Missing authorization code'); + } + + $params = array( + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'grant_type' => isset($options['grantType']) ? $options['grantType'] : 'authorization_code', + ); + + switch ($params['grant_type']) { + + case 'authorization_code': + $params['code'] = $code; + $params['redirect_uri'] = isset($options['redirectUri']) ? $options['redirectUri'] : $this->redirectUri; + break; + + case 'refresh_token': + $params['refresh_token'] = $code; + break; + + } + + try { + + switch ($this->method) { + + case 'get': + + $client = new GuzzleClient($this->urlAccessToken() . '?' . http_build_query($params)); + $request = $client->send(); + $response = $request->getBody(); + + break; + + case 'post': + + $client = new GuzzleClient($this->urlAccessToken()); + $request = $client->post(null, null, $params)->send(); + $response = $request->getBody(); + + break; + + } + + } + + catch (\Guzzle\Http\Exception\BadResponseException $e) + { + $raw_response = explode("\n", $e->getResponse()); + $response = end($raw_response); + } + + switch ($this->responseType) { + + case 'json': + $result = json_decode($response, true); + break; + + case 'string': + parse_str($response, $result); + break; + + } + + if (isset($result['error']) && ! empty($result['error'])) { + + throw new \Oauth2\Client\IDPException($result); + + } + + switch ($params['grant_type']) { + + case 'authorization_code': + return \Oauth2\Client\Token::factory('access', $result); + break; + + case 'refresh_token': + return \Oauth2\Client\Token::factory('refresh', $result); + break; + + } + } + + public function getUserDetails(\Oauth2\Client\Token\Access $token) + { + $url = $this->urlUserDetails($token); + + try { + $client = new GuzzleClient($url); + $request = $client->get()->send(); + $response = $request->getBody(); + + return $this->userDetails(json_decode($response), $token); + } + + catch (\Guzzle\Http\Exception\BadResponseException $e) + { + $raw_response = explode("\n", $e->getResponse()); + throw new \Oauth2\Client\IDPException(end($raw_response)); + } + } + +} \ No newline at end of file diff --git a/src/Oauth2/Client/Provider/Blooie.php b/src/Oauth2/Client/Provider/Blooie.php new file mode 100755 index 00000000..8078690e --- /dev/null +++ b/src/Oauth2/Client/Provider/Blooie.php @@ -0,0 +1,42 @@ + $token->access_token, + )); + + $user = json_decode(file_get_contents($url)); + + return array( + 'uid' => $user->id, + 'nickname' => $user->username, + 'name' => $user->name, + 'first_name' => $user->first_name, + 'last_name' => $user->last_name, + 'email' => isset($user->email) ? $user->email : null, + 'location' => isset($user->hometown->name) ? $user->hometown->name : null, + 'description' => isset($user->bio) ? $user->bio : null, + 'image' => 'https://graph.facebook.com/me/picture?type=normal&access_token='.$token->access_token, + 'urls' => array( + 'Facebook' => $user->link, + ), + ); + } +} diff --git a/src/Oauth2/Client/Provider/Facebook.php b/src/Oauth2/Client/Provider/Facebook.php new file mode 100755 index 00000000..53aba543 --- /dev/null +++ b/src/Oauth2/Client/Provider/Facebook.php @@ -0,0 +1,49 @@ + $token->access_token, + )); + + $user = json_decode(file_get_contents($url)); + + return array( + 'uid' => $user->id, + 'nickname' => isset($user->username) ? $user->username : null, + 'name' => $user->name, + 'first_name' => $user->first_name, + 'last_name' => $user->last_name, + 'email' => isset($user->email) ? $user->email : null, + 'location' => isset($user->hometown->name) ? $user->hometown->name : null, + 'description' => isset($user->bio) ? $user->bio : null, + 'image' => 'https://graph.facebook.com/me/picture?type=normal&access_token='.$token->access_token, + 'urls' => array( + 'Facebook' => $user->link, + ), + ); + } +} diff --git a/src/Oauth2/Client/Provider/Foursquare.php b/src/Oauth2/Client/Provider/Foursquare.php new file mode 100755 index 00000000..ebf81cb1 --- /dev/null +++ b/src/Oauth2/Client/Provider/Foursquare.php @@ -0,0 +1,45 @@ + $token->access_token, + )); + + $response = json_decode(file_get_contents($url)); + + $user = $response->response->user; + + // Create a response from the request + return array( + 'uid' => $user->id, + 'name' => sprintf('%s %s', $user->firstName, $user->lastName), + 'email' => $user->contact->email, + 'image' => $user->photo, + 'location' => $user->homeCity, + ); + } +} \ No newline at end of file diff --git a/src/Oauth2/Client/Provider/Github.php b/src/Oauth2/Client/Provider/Github.php new file mode 100755 index 00000000..acfd59ff --- /dev/null +++ b/src/Oauth2/Client/Provider/Github.php @@ -0,0 +1,43 @@ + $token->access_token, + )); + + $user = json_decode(file_get_contents($url)); + + return array( + 'uid' => $user->id, + 'nickname' => $user->login, + 'name' => $user->name, + 'email' => $user->email, + 'urls' => array( + 'GitHub' => 'http://github.com/'.$user->login, + 'Blog' => $user->blog, + ), + ); + } +} \ No newline at end of file diff --git a/src/Oauth2/Client/Provider/Google.php b/src/Oauth2/Client/Provider/Google.php new file mode 100755 index 00000000..b76d343b --- /dev/null +++ b/src/Oauth2/Client/Provider/Google.php @@ -0,0 +1,84 @@ + 'Expected Authorization Code from '.ucfirst($this->name).' is missing')); + } + + return parent::access($code, $options); + } + + public function get_user_info(OAuth2_Token_Access $token) + { + $url = 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json&'.http_build_query(array( + 'access_token' => $token->access_token, + )); + + $user = json_decode(file_get_contents($url), true); + return array( + 'uid' => $user['id'], + 'nickname' => url_title($user['name'], '_', true), + 'name' => $user['name'], + 'first_name' => $user['given_name'], + 'last_name' => $user['family_name'], + 'email' => $user['email'], + 'location' => null, + 'image' => (isset($user['picture'])) ? $user['picture'] : null, + 'description' => null, + 'urls' => array(), + ); + } +} diff --git a/src/Oauth2/Client/Provider/Instagram.php b/src/Oauth2/Client/Provider/Instagram.php new file mode 100755 index 00000000..39c8bef1 --- /dev/null +++ b/src/Oauth2/Client/Provider/Instagram.php @@ -0,0 +1,48 @@ +user; + + return array( + 'uid' => $user->id, + 'nickname' => $user->username, + 'name' => $user->full_name, + 'image' => $user->profile_picture, + 'urls' => array( + 'website' => $user->website, + ), + ); + } +} \ No newline at end of file diff --git a/src/Oauth2/Client/Provider/Mailchimp.php b/src/Oauth2/Client/Provider/Mailchimp.php new file mode 100755 index 00000000..818bd40d --- /dev/null +++ b/src/Oauth2/Client/Provider/Mailchimp.php @@ -0,0 +1,36 @@ + $token->access_token, + ); + } +} diff --git a/src/Oauth2/Client/Provider/Mailru.php b/src/Oauth2/Client/Provider/Mailru.php new file mode 100755 index 00000000..25234a20 --- /dev/null +++ b/src/Oauth2/Client/Provider/Mailru.php @@ -0,0 +1,73 @@ + $value) { + $params .= "$key=$value"; + } + return md5($params . $secret_key); + } + + public function get_user_info(OAuth2_Token_Access $token) + { + $request_params = array( + 'app_id' => $this->client_id, + 'method' => 'users.getInfo', + 'uids' => $token->uid, + 'access_token' => $token->access_token, + 'secure' => 1 + ); + + $sig = $this->sign_server_server($request_params,$this->client_secret); + $url = 'http://www.appsmail.ru/platform/api?'.http_build_query($request_params).'&sig='.$sig; + + $user = json_decode(file_get_contents($url)); + + return array( + 'uid' => $user[0]->uid, + 'nickname' => $user[0]->nick, + 'name' => $user[0]->first_name.' '.$user[0]->last_name, + 'first_name' => $user[0]->first_name, + 'last_name' => $user[0]->last_name, + 'email' => isset($user[0]->email) ? $user[0]->email : null, + 'image' => isset($user[0]->pic_big) ? $user[0]->pic_big : null, + ); + } + + public function authorize($options = array()) + { + $state = md5(uniqid(rand(), TRUE)); + get_instance()->session->set_userdata('state', $state); + + $params = array( + 'client_id' => $this->client_id, + 'redirect_uri' => isset($options['redirect_uri']) ? $options['redirect_uri'] : $this->redirect_uri, + 'response_type' => 'code', + ); + + redirect($this->url_authorize().'?'.http_build_query($params)); + } +} diff --git a/src/Oauth2/Client/Provider/Paypal.php b/src/Oauth2/Client/Provider/Paypal.php new file mode 100755 index 00000000..72d065fe --- /dev/null +++ b/src/Oauth2/Client/Provider/Paypal.php @@ -0,0 +1,59 @@ + $token->access_token + )); + + $user = json_decode(file_get_contents($url)); + $user = $user->identity; + + return array( + 'uid' => $user['userId'], + 'nickname' => url_title($user['fullName'], '_', true), + 'name' => $user['fullName'], + 'first_name' => $user['firstName'], + 'last_name' => $user['lastName'], + 'email' => $user['emails'][0], + 'location' => $user->addresses[0], + 'image' => null, + 'description' => null, + 'urls' => array( + 'PayPal' => null + ) + ); + } + +} diff --git a/src/Oauth2/Client/Provider/Soundcloud.php b/src/Oauth2/Client/Provider/Soundcloud.php new file mode 100755 index 00000000..83d9afbc --- /dev/null +++ b/src/Oauth2/Client/Provider/Soundcloud.php @@ -0,0 +1,51 @@ + $token->access_token, + )); + + $user = json_decode(file_get_contents($url)); + + // Create a response from the request + return array( + 'uid' => $user->id, + 'nickname' => $user->username, + 'name' => $user->full_name, + 'location' => $user->country.' ,'.$user->country, + 'description' => $user->description, + 'image' => $user->avatar_url, + 'urls' => array( + 'MySpace' => $user->myspace_name, + 'Website' => $user->website, + ), + ); + } +} diff --git a/src/Oauth2/Client/Provider/Vkontakte.php b/src/Oauth2/Client/Provider/Vkontakte.php new file mode 100755 index 00000000..bdad78e4 --- /dev/null +++ b/src/Oauth2/Client/Provider/Vkontakte.php @@ -0,0 +1,54 @@ + $token->uid, + 'fields' => implode(",",$scope), + 'access_token' => $token->access_token, + )); + + $user = json_decode(file_get_contents($url))->response; + + if(sizeof($user)==0) + return null; + else + $user = $user[0]; + + return array( + 'uid' => $user->uid, + 'nickname' => isset($user->nickname) ? $user->nickname : null, + 'name' => isset($user->name) ? $user->name : null, + 'first_name' => isset($user->first_name) ? $user->first_name : null, + 'last_name' => isset($user->last_name) ? $user->last_name : null, + 'email' => null, + 'location' => null, + 'description' => null, + 'image' => isset($user->photo_big) ? $user->photo_big : null, + 'urls' => array(), + ); + } +} diff --git a/src/Oauth2/Client/Provider/Windowslive.php b/src/Oauth2/Client/Provider/Windowslive.php new file mode 100755 index 00000000..8d489c3d --- /dev/null +++ b/src/Oauth2/Client/Provider/Windowslive.php @@ -0,0 +1,60 @@ + $token->access_token, + )); + + // perform network request + $user = json_decode(file_get_contents($url)); + + // create a response from the request and return it + return array( + 'uid' => $user->id, + 'name' => $user->name, + 'nickname' => url_title($user->name, '_', true), +// 'location' => $user[''], # scope wl.postal_addresses is required + # but won't be implemented by default + 'locale' => $user->locale, + 'urls' => array('Windows Live' => $user->link), + ); + } +} diff --git a/src/Oauth2/Client/Provider/Yandex.php b/src/Oauth2/Client/Provider/Yandex.php new file mode 100755 index 00000000..7a8f4f80 --- /dev/null +++ b/src/Oauth2/Client/Provider/Yandex.php @@ -0,0 +1,115 @@ + array( + 'method' => 'GET', + 'header' => 'Authorization: OAuth '.$token->access_token + ) + ); + $_default_opts = stream_context_get_params(stream_context_get_default()); + + $opts = array_merge_recursive($_default_opts['options'], $opts); + $context = stream_context_create($opts); + $url = 'http://api-yaru.yandex.ru/me/?format=json'; + + $user = json_decode(file_get_contents($url,false,$context)); + + preg_match("/\d+$/",$user->id,$uid); + + return array( + 'uid' => $uid[0], + 'nickname' => isset($user->name) ? $user->name : null, + 'name' => isset($user->name) ? $user->name : null, + 'first_name' => isset($user->first_name) ? $user->first_name : null, + 'last_name' => isset($user->last_name) ? $user->last_name : null, + 'email' => isset($user->email) ? $user->email : null, + 'location' => isset($user->hometown->name) ? $user->hometown->name : null, + 'description' => isset($user->bio) ? $user->bio : null, + 'image' => $user->links->userpic, + ); + } + + public function access($code, $options = array()) + { + $params = array( + 'client_id' => $this->client_id, + 'client_secret' => $this->client_secret, + 'grant_type' => isset($options['grant_type']) ? $options['grant_type'] : 'authorization_code', + ); + + switch ($params['grant_type']) + { + case 'authorization_code': + $params['code'] = $code; + $params['redirect_uri'] = isset($options['redirect_uri']) ? $options['redirect_uri'] : $this->redirect_uri; + break; + + case 'refresh_token': + $params['refresh_token'] = $code; + break; + } + + $response = null; + $url = $this->url_access_token(); + + $curl = curl_init($url); + + $headers[] = 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8;'; + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + +// curl_setopt($curl, CURLOPT_USERAGENT, 'yamolib-php'); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 30); + curl_setopt($curl, CURLOPT_TIMEOUT, 80); + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($params)); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); + // curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true); + // curl_setopt($curl, CURLOPT_CAINFO, dirname(__FILE__) . '/../data/ca-certificate.crt'); + + $response = curl_exec($curl); + curl_close($curl); + + $return = json_decode($response, true); + + if ( ! empty($return['error'])) + { + throw new OAuth2_Exception($return); + } + + switch ($params['grant_type']) + { + case 'authorization_code': + return OAuth2_Token::factory('access', $return); + break; + + case 'refresh_token': + return OAuth2_Token::factory('refresh', $return); + break; + } + } + +} diff --git a/src/Oauth2/Client/Token.php b/src/Oauth2/Client/Token.php new file mode 100755 index 00000000..2cb15b12 --- /dev/null +++ b/src/Oauth2/Client/Token.php @@ -0,0 +1,46 @@ +$key; + } + + /** + * Return a boolean if the property is set + * + * @param string variable name + * @return bool + */ + public function __isset($key) + { + return isset($this->$key); + } + +} // End Token diff --git a/src/Oauth2/Client/Token/Access.php b/src/Oauth2/Client/Token/Access.php new file mode 100755 index 00000000..18d3cd49 --- /dev/null +++ b/src/Oauth2/Client/Token/Access.php @@ -0,0 +1,79 @@ +accessToken = $options['access_token']; + + // Some providers (not many) give the uid here, so lets take it + isset($options['uid']) and $this->uid = $options['uid']; + + //Vkontakte uses user_id instead of uid + isset($options['user_id']) and $this->uid = $options['user_id']; + + //Mailru uses x_mailru_vid instead of uid + isset($options['x_mailru_vid']) and $this->uid = $options['x_mailru_vid']; + + // We need to know when the token expires, add num. seconds to current time + isset($options['expires_in']) and $this->expires = time() + ((int) $options['expires_in']); + + // Facebook is just being a spec ignoring jerk + isset($options['expires']) and $this->expires = time() + ((int) $options['expires']); + + // Grab a refresh token so we can update access tokens when they expires + isset($options['refresh_token']) and $this->refreshToken = $options['refresh_token']; + } + + /** + * Returns the token key. + * + * @return string + */ + public function __toString() + { + return (string) $this->accessToken; + } + +} \ No newline at end of file diff --git a/src/Oauth2/Client/Token/Authorize.php b/src/Oauth2/Client/Token/Authorize.php new file mode 100755 index 00000000..f9358385 --- /dev/null +++ b/src/Oauth2/Client/Token/Authorize.php @@ -0,0 +1,55 @@ +code = $options['code']; + $this->redirectUri = $options['redirect_uri']; + } + + /** + * Returns the token key. + * + * @return string + */ + public function __toString() + { + return (string) $this->code; + } + +} \ No newline at end of file