From fa6c7852b06fd5b0f6f4a58018f53056f864628d Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Sat, 14 Jun 2014 13:17:42 +0200 Subject: [PATCH 1/2] Add support for two fatcor authentication --- lib/Github/Api/Authorizations.php | 6 ++-- ...oFactorAuthenticationRequiredException.php | 19 +++++++++++ lib/Github/HttpClient/HttpClient.php | 7 ++-- .../HttpClient/Listener/ErrorListener.php | 9 ++++++ .../Tests/HttpClient/HttpClientTest.php | 23 +++++++++++++ .../HttpClient/Listener/ErrorListenerTest.php | 32 +++++++++++++++++++ 6 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 lib/Github/Exception/TwoFactorAuthenticationRequiredException.php diff --git a/lib/Github/Api/Authorizations.php b/lib/Github/Api/Authorizations.php index 0066a1f0f3f..68f9414fd0c 100644 --- a/lib/Github/Api/Authorizations.php +++ b/lib/Github/Api/Authorizations.php @@ -21,9 +21,11 @@ public function show($number) return $this->get('authorizations/'.rawurlencode($number)); } - public function create(array $params) + public function create(array $params, $OTPCode = null) { - return $this->post('authorizations', $params); + $headers = null === $OTPCode ? array() : array('X-GitHub-OTP' => $OTPCode); + + return $this->post('authorizations', $params, $headers); } public function update($id, array $params) diff --git a/lib/Github/Exception/TwoFactorAuthenticationRequiredException.php b/lib/Github/Exception/TwoFactorAuthenticationRequiredException.php new file mode 100644 index 00000000000..6f93fe40b14 --- /dev/null +++ b/lib/Github/Exception/TwoFactorAuthenticationRequiredException.php @@ -0,0 +1,19 @@ +type = $type; + parent::__construct('Two factor authentication is enabled on this account', $code, $previous); + } + + public function getType() + { + return $this->type; + } +} diff --git a/lib/Github/HttpClient/HttpClient.php b/lib/Github/HttpClient/HttpClient.php index 59b3cce759e..2ba4d9cf873 100644 --- a/lib/Github/HttpClient/HttpClient.php +++ b/lib/Github/HttpClient/HttpClient.php @@ -2,6 +2,7 @@ namespace Github\HttpClient; +use Github\Exception\TwoFactorAuthenticationRequiredException; use Guzzle\Http\Client as GuzzleClient; use Guzzle\Http\ClientInterface; use Guzzle\Http\Message\Request; @@ -139,9 +140,11 @@ public function request($path, $body = null, $httpMethod = 'GET', array $headers try { $response = $this->client->send($request); } catch (\LogicException $e) { - throw new ErrorException($e->getMessage()); + throw new ErrorException($e->getMessage(), $e->getCode(), $e); + } catch (TwoFactorAuthenticationRequiredException $e) { + throw $e; } catch (\RuntimeException $e) { - throw new RuntimeException($e->getMessage()); + throw new RuntimeException($e->getMessage(), $e->getCode(), $e); } $this->lastRequest = $request; diff --git a/lib/Github/HttpClient/Listener/ErrorListener.php b/lib/Github/HttpClient/Listener/ErrorListener.php index 60cbc5a80fd..30106178d17 100644 --- a/lib/Github/HttpClient/Listener/ErrorListener.php +++ b/lib/Github/HttpClient/Listener/ErrorListener.php @@ -2,6 +2,7 @@ namespace Github\HttpClient\Listener; +use Github\Exception\TwoFactorAuthenticationRequiredException; use Github\HttpClient\Message\ResponseMediator; use Guzzle\Common\Event; use Guzzle\Http\Message\Response; @@ -45,6 +46,14 @@ public function onRequestError(Event $event) throw new ApiLimitExceedException($this->options['api_limit']); } + if (401 === $response->getStatusCode()) { + if ($response->hasHeader('X-GitHub-OTP') && 0 === strpos((string) $response->getHeader('X-GitHub-OTP'), 'required;')) { + $type = substr((string) $response->getHeader('X-GitHub-OTP'), 9); + + throw new TwoFactorAuthenticationRequiredException($type); + } + } + $content = ResponseMediator::getContent($response); if (is_array($content) && isset($content['message'])) { if (400 == $response->getStatusCode()) { diff --git a/test/Github/Tests/HttpClient/HttpClientTest.php b/test/Github/Tests/HttpClient/HttpClientTest.php index cb83e9af96d..c6a645a214f 100644 --- a/test/Github/Tests/HttpClient/HttpClientTest.php +++ b/test/Github/Tests/HttpClient/HttpClientTest.php @@ -242,6 +242,29 @@ public function shouldThrowExceptionWhenApiIsExceeded() $httpClient->get($path, $parameters, $headers); } + /** + * @test + * @expectedException \Github\Exception\TwoFactorAuthenticationRequiredException + */ + public function shouldForwardTwoFactorAuthenticationExceptionWhenItHappens() + { + $path = '/some/path'; + $parameters = array('a = b'); + $headers = array('c' => 'd'); + + $response = new Response(401); + $response->addHeader('X-GitHub-OTP', 'required; sms'); + + $mockPlugin = new MockPlugin(); + $mockPlugin->addResponse($response); + + $client = new GuzzleClient('http://123.com/'); + $client->addSubscriber($mockPlugin); + + $httpClient = new TestHttpClient(array(), $client); + $httpClient->get($path, $parameters, $headers); + } + protected function getBrowserMock(array $methods = array()) { $mock = $this->getMock( diff --git a/test/Github/Tests/HttpClient/Listener/ErrorListenerTest.php b/test/Github/Tests/HttpClient/Listener/ErrorListenerTest.php index 8bb0bfa177f..3715ee57d3d 100644 --- a/test/Github/Tests/HttpClient/Listener/ErrorListenerTest.php +++ b/test/Github/Tests/HttpClient/Listener/ErrorListenerTest.php @@ -149,6 +149,38 @@ public function shouldNotPassWhen422IsSentWithErrorCode($errorCode) $listener->onRequestError($this->getEventMock($response)); } + /** + * @test + * @expectedException \Github\Exception\TwoFactorAuthenticationRequiredException + */ + public function shouldThrowTwoFactorAuthenticationRequiredException() + { + $response = $this->getMockBuilder('Guzzle\Http\Message\Response')->disableOriginalConstructor()->getMock(); + $response->expects($this->once()) + ->method('isClientError') + ->will($this->returnValue(true)); + $response->expects($this->any()) + ->method('getStatusCode') + ->will($this->returnValue(401)); + $response->expects($this->any()) + ->method('getHeader') + ->will($this->returnCallback(function ($name) { + switch ($name) { + case 'X-RateLimit-Remaining': + return 5000; + case 'X-GitHub-OTP': + return 'required; sms'; + } + })); + $response->expects($this->any()) + ->method('hasHeader') + ->with('X-GitHub-OTP') + ->will($this->returnValue(true)); + + $listener = new ErrorListener(array()); + $listener->onRequestError($this->getEventMock($response)); + } + public function getErrorCodesProvider() { return array( From 2958ad646451d0fff0be4e8ba16a1ee1eed80755 Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Sat, 14 Jun 2014 13:26:34 +0200 Subject: [PATCH 2/2] Add documentation for two factor authentication --- doc/two_factor_authentication.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 doc/two_factor_authentication.md diff --git a/doc/two_factor_authentication.md b/doc/two_factor_authentication.md new file mode 100644 index 00000000000..2b670b05ba2 --- /dev/null +++ b/doc/two_factor_authentication.md @@ -0,0 +1,19 @@ +## Two factor authentication +[Back to the navigation](index.md) + + +### Raising the exception + +```php +try { + $authorization = $github->api('authorizations')->create(); +} catch (Github\Exception\TwoFactorAuthenticationRequiredException $e { + echo sprintf("Two factor authentication of type %s is required.", $e->getType()); +} +``` + +Once the code has been retrieved (by sms for example), you can create an authorization: + +``` +$authorization = $github->api('authorizations')->create(array('note' => 'Optional'), $code); +```