From 12036aa21f4308aca4d816864b357f9627a0f437 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Thu, 16 Jul 2015 07:28:46 -0400 Subject: Refactoring of Google Authentication (new callback url) --- app/Auth/Google.php | 78 +++++++++-------------- app/Controller/Oauth.php | 114 ++++++++++++++++++++++++++++++++++ app/Controller/User.php | 63 ------------------- app/Core/HttpClient.php | 21 ++++++- app/Core/OAuth2.php | 75 ++++++++++++++++++++++ app/Model/Acl.php | 3 +- app/ServiceProvider/ClassProvider.php | 5 ++ app/Template/auth/index.php | 2 +- app/Template/user/external.php | 4 +- 9 files changed, 247 insertions(+), 118 deletions(-) create mode 100644 app/Controller/Oauth.php create mode 100644 app/Core/OAuth2.php diff --git a/app/Auth/Google.php b/app/Auth/Google.php index 9a977037..dd8f3834 100644 --- a/app/Auth/Google.php +++ b/app/Auth/Google.php @@ -3,11 +3,6 @@ namespace Auth; use Event\AuthEvent; -use OAuth\Common\Storage\Session; -use OAuth\Common\Consumer\Credentials; -use OAuth\Common\Http\Uri\UriFactory; -use OAuth\ServiceFactory; -use OAuth\Common\Http\Exception\TokenResponseException; /** * Google backend @@ -24,6 +19,13 @@ class Google extends Base */ const AUTH_NAME = 'Google'; + /** + * OAuth2 instance + * + * @var \Core\OAuth2 + */ + private $service; + /** * Authenticate a Google user * @@ -78,63 +80,41 @@ class Google extends Base } /** - * Get the Google service instance + * Get OAuth2 configured service * * @access public - * @return \OAuth\OAuth2\Service\Google + * @return \Core\OAuth2 */ public function getService() { - $uriFactory = new UriFactory(); - $currentUri = $uriFactory->createFromSuperGlobalArray($_SERVER); - $currentUri->setQuery('controller=user&action=google'); - - $storage = new Session(false); - - $credentials = new Credentials( - GOOGLE_CLIENT_ID, - GOOGLE_CLIENT_SECRET, - $currentUri->getAbsoluteUri() - ); - - $serviceFactory = new ServiceFactory(); - - return $serviceFactory->createService( - 'google', - $credentials, - $storage, - array('userinfo_email', 'userinfo_profile') - ); - } + if (empty($this->service)) { + $this->service = $this->oauth->createService( + GOOGLE_CLIENT_ID, + GOOGLE_CLIENT_SECRET, + $this->helper->url->to('oauth', 'google', array(), '', true), + 'https://accounts.google.com/o/oauth2/auth', + 'https://accounts.google.com/o/oauth2/token', + array('https://www.googleapis.com/auth/userinfo.email', 'https://www.googleapis.com/auth/userinfo.profile') + ); + } - /** - * Get the authorization URL - * - * @access public - * @return \OAuth\Common\Http\Uri\Uri - */ - public function getAuthorizationUrl() - { - return $this->getService()->getAuthorizationUri(); + return $this->service; } /** - * Get Google profile information from the API + * Get Google profile * * @access public - * @param string $code Google authorization code - * @return bool|array + * @param string $code + * @return array */ - public function getGoogleProfile($code) + public function getProfile($code) { - try { + $this->getService()->getAccessToken($code); - $googleService = $this->getService(); - $googleService->requestAccessToken($code); - return json_decode($googleService->request('https://www.googleapis.com/oauth2/v1/userinfo'), true); - } - catch (TokenResponseException $e) { - return false; - } + return $this->httpClient->getJson( + 'https://www.googleapis.com/oauth2/v1/userinfo', + array($this->getService()->getAuthorizationHeader() + )); } } diff --git a/app/Controller/Oauth.php b/app/Controller/Oauth.php new file mode 100644 index 00000000..32947d1a --- /dev/null +++ b/app/Controller/Oauth.php @@ -0,0 +1,114 @@ +step1('google'); + } + + /** + * Unlink external account + * + * @access public + */ + public function unlink($backend = '') + { + $backend = $this->request->getStringParam('backend', $backend); + $this->checkCSRFParam(); + + if ($this->authentication->backend($backend)->unlink($this->userSession->getId())) { + $this->session->flash(t('Your external account is not linked anymore to your profile.')); + } + else { + $this->session->flashError(t('Unable to unlink your external account.')); + } + + $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId()))); + } + + /** + * Redirect to the provider if no code received + * + * @access private + */ + private function step1($backend) + { + $code = $this->request->getStringParam('code'); + + if (! empty($code)) { + $this->step2($backend, $code); + } + else { + $this->response->redirect($this->authentication->backend($backend)->getService()->getAuthorizationUrl()); + } + } + + /** + * Link or authenticate the user + * + * @access private + */ + private function step2($backend, $code) + { + $profile = $this->authentication->backend($backend)->getProfile($code); + + if ($this->userSession->isLogged()) { + $this->link($backend, $profile); + } + + $this->authenticate($backend, $profile); + } + + /** + * Link the account + * + * @access private + */ + private function link($backend, $profile) + { + if (empty($profile)) { + $this->session->flashError(t('External authentication failed')); + } + else { + $this->session->flash(t('Your external account is linked to your profile successfully.')); + $this->authentication->backend($backend)->updateUser($this->userSession->getId(), $profile); + } + + $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId()))); + } + + /** + * Authenticate the account + * + * @access private + */ + private function authenticate($backend, $profile) + { + if (! empty($profile) && $this->authentication->backend($backend)->authenticate($profile['id'])) { + $this->response->redirect($this->helper->url->to('app', 'index')); + } + else { + $this->response->html($this->template->layout('auth/index', array( + 'errors' => array('login' => t('External authentication failed')), + 'values' => array(), + 'no_layout' => true, + 'redirect_query' => '', + 'title' => t('Login') + ))); + } + } +} diff --git a/app/Controller/User.php b/app/Controller/User.php index edebf5ba..7a17d98e 100644 --- a/app/Controller/User.php +++ b/app/Controller/User.php @@ -361,69 +361,6 @@ class User extends Base ))); } - /** - * Google authentication - * - * @access public - */ - public function google() - { - $code = $this->request->getStringParam('code'); - - if ($code) { - - $profile = $this->authentication->backend('google')->getGoogleProfile($code); - - if (is_array($profile)) { - - // If the user is already logged, link the account otherwise authenticate - if ($this->userSession->isLogged()) { - - if ($this->authentication->backend('google')->updateUser($this->userSession->getId(), $profile)) { - $this->session->flash(t('Your Google Account is linked to your profile successfully.')); - } - else { - $this->session->flashError(t('Unable to link your Google Account.')); - } - - $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId()))); - } - else if ($this->authentication->backend('google')->authenticate($profile['id'])) { - $this->response->redirect($this->helper->url->to('app', 'index')); - } - else { - $this->response->html($this->template->layout('auth/index', array( - 'errors' => array('login' => t('Google authentication failed')), - 'values' => array(), - 'no_layout' => true, - 'redirect_query' => '', - 'title' => t('Login') - ))); - } - } - } - - $this->response->redirect($this->authentication->backend('google')->getAuthorizationUrl()); - } - - /** - * Unlink a Google account - * - * @access public - */ - public function unlinkGoogle() - { - $this->checkCSRFParam(); - if ($this->authentication->backend('google')->unlink($this->userSession->getId())) { - $this->session->flash(t('Your Google Account is not linked anymore to your profile.')); - } - else { - $this->session->flashError(t('Unable to unlink your Google Account.')); - } - - $this->response->redirect($this->helper->url->to('user', 'external', array('user_id' => $this->userSession->getId()))); - } - /** * GitHub authentication * diff --git a/app/Core/HttpClient.php b/app/Core/HttpClient.php index 805c1e5a..b808f756 100644 --- a/app/Core/HttpClient.php +++ b/app/Core/HttpClient.php @@ -31,6 +31,20 @@ class HttpClient extends Base */ const HTTP_USER_AGENT = 'Kanboard'; + /** + * Send a GET HTTP request and parse JSON response + * + * @access public + * @param string $url + * @param string[] $headers + * @return array + */ + public function getJson($url, array $headers = array()) + { + $response = $this->doRequest('GET', $url, '', array_merge(array('Accept: application/json'), $headers)); + return json_decode($response, true) ?: array(); + } + /** * Send a POST HTTP request encoded in JSON * @@ -43,6 +57,7 @@ class HttpClient extends Base public function postJson($url, array $data, array $headers = array()) { return $this->doRequest( + 'POST', $url, json_encode($data), array_merge(array('Content-type: application/json'), $headers) @@ -61,6 +76,7 @@ class HttpClient extends Base public function postForm($url, array $data, array $headers = array()) { return $this->doRequest( + 'POST', $url, http_build_query($data), array_merge(array('Content-type: application/x-www-form-urlencoded'), $headers) @@ -71,12 +87,13 @@ class HttpClient extends Base * Make the HTTP request * * @access private + * @param string $method * @param string $url * @param string $content * @param string[] $headers * @return string */ - private function doRequest($url, $content, array $headers) + private function doRequest($method, $url, $content, array $headers) { if (empty($url)) { return ''; @@ -86,7 +103,7 @@ class HttpClient extends Base $context = stream_context_create(array( 'http' => array( - 'method' => 'POST', + 'method' => $method, 'protocol_version' => 1.1, 'timeout' => self::HTTP_TIMEOUT, 'max_redirects' => self::HTTP_MAX_REDIRECTS, diff --git a/app/Core/OAuth2.php b/app/Core/OAuth2.php new file mode 100644 index 00000000..a0b33e31 --- /dev/null +++ b/app/Core/OAuth2.php @@ -0,0 +1,75 @@ +clientId = $clientId; + $this->secret = $secret; + $this->callbackUrl = $callbackUrl; + $this->authUrl = $authUrl; + $this->tokenUrl = $tokenUrl; + $this->scopes = $scopes; + + return $this; + } + + public function getAuthorizationUrl() + { + $params = array( + 'response_type' => 'code', + 'client_id' => $this->clientId, + 'redirect_uri' => $this->callbackUrl, + 'scope' => implode(' ', $this->scopes), + ); + + return $this->authUrl.'?'.http_build_query($params); + } + + public function getAuthorizationHeader() + { + if ($this->tokenType === 'Bearer') { + return 'Authorization: Bearer '.$this->accessToken; + } + + return ''; + } + + public function getAccessToken($code) + { + if (empty($this->accessToken) && ! empty($code)) { + + $params = array( + 'code' => $code, + 'client_id' => $this->clientId, + 'client_secret' => $this->secret, + 'redirect_uri' => $this->callbackUrl, + 'grant_type' => 'authorization_code', + ); + + $response = json_decode($this->httpClient->postForm($this->tokenUrl, $params), true); + + $this->tokenType = isset($response['token_type']) ? $response['token_type'] : ''; + $this->accessToken = isset($response['access_token']) ? $response['access_token'] : ''; + } + + return $this->accessToken; + } +} diff --git a/app/Model/Acl.php b/app/Model/Acl.php index 09638302..579b5d90 100644 --- a/app/Model/Acl.php +++ b/app/Model/Acl.php @@ -18,12 +18,13 @@ class Acl extends Base */ private $public_acl = array( 'auth' => array('login', 'check'), - 'user' => array('google', 'github'), + 'user' => array('github'), 'task' => array('readonly'), 'board' => array('readonly'), 'webhook' => '*', 'ical' => '*', 'feed' => '*', + 'oauth' => array('google'), ); /** diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index ef772f58..ef7aa575 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -3,6 +3,7 @@ namespace ServiceProvider; use Core\Paginator; +use Core\OAuth2; use Model\Config; use Model\Project; use Model\Webhook; @@ -107,5 +108,9 @@ class ClassProvider implements ServiceProviderInterface $container['paginator'] = $container->factory(function ($c) { return new Paginator($c); }); + + $container['oauth'] = $container->factory(function ($c) { + return new OAuth2($c); + }); } } diff --git a/app/Template/auth/index.php b/app/Template/auth/index.php index 39d007f5..ed923f07 100644 --- a/app/Template/auth/index.php +++ b/app/Template/auth/index.php @@ -17,7 +17,7 @@ form->checkbox('remember_me', t('Remember Me'), 1, true) ?>
- url->link(t('Login with my Google Account'), 'user', 'google') ?> + url->link(t('Login with my Google Account'), 'oauth', 'google') ?> diff --git a/app/Template/user/external.php b/app/Template/user/external.php index df85ace7..18d40d79 100644 --- a/app/Template/user/external.php +++ b/app/Template/user/external.php @@ -8,9 +8,9 @@

user->isCurrentUser($user['id'])): ?> - url->link(t('Link my Google Account'), 'user', 'google', array(), true) ?> + url->link(t('Link my Google Account'), 'oauth', 'google', array(), true) ?> - url->link(t('Unlink my Google Account'), 'user', 'unlinkGoogle', array(), true) ?> + url->link(t('Unlink my Google Account'), 'oauth', 'unlink', array('backend' => 'google'), true) ?> -- cgit v1.2.3