Skip to content

Commit 513266b

Browse files
committed
Merge pull request #138 from romainneutron/2fa
Add support for two factor authentication
2 parents bdb8637 + 2958ad6 commit 513266b

File tree

7 files changed

+111
-4
lines changed

7 files changed

+111
-4
lines changed

doc/two_factor_authentication.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Two factor authentication
2+
[Back to the navigation](index.md)
3+
4+
5+
### Raising the exception
6+
7+
```php
8+
try {
9+
$authorization = $github->api('authorizations')->create();
10+
} catch (Github\Exception\TwoFactorAuthenticationRequiredException $e {
11+
echo sprintf("Two factor authentication of type %s is required.", $e->getType());
12+
}
13+
```
14+
15+
Once the code has been retrieved (by sms for example), you can create an authorization:
16+
17+
```
18+
$authorization = $github->api('authorizations')->create(array('note' => 'Optional'), $code);
19+
```

lib/Github/Api/Authorizations.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ public function show($number)
2121
return $this->get('authorizations/'.rawurlencode($number));
2222
}
2323

24-
public function create(array $params)
24+
public function create(array $params, $OTPCode = null)
2525
{
26-
return $this->post('authorizations', $params);
26+
$headers = null === $OTPCode ? array() : array('X-GitHub-OTP' => $OTPCode);
27+
28+
return $this->post('authorizations', $params, $headers);
2729
}
2830

2931
public function update($id, array $params)
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Github\Exception;
4+
5+
class TwoFactorAuthenticationRequiredException extends RuntimeException
6+
{
7+
private $type;
8+
9+
public function __construct($type, $code = 0, $previous = null)
10+
{
11+
$this->type = $type;
12+
parent::__construct('Two factor authentication is enabled on this account', $code, $previous);
13+
}
14+
15+
public function getType()
16+
{
17+
return $this->type;
18+
}
19+
}

lib/Github/HttpClient/HttpClient.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Github\HttpClient;
44

5+
use Github\Exception\TwoFactorAuthenticationRequiredException;
56
use Guzzle\Http\Client as GuzzleClient;
67
use Guzzle\Http\ClientInterface;
78
use Guzzle\Http\Message\Request;
@@ -139,9 +140,11 @@ public function request($path, $body = null, $httpMethod = 'GET', array $headers
139140
try {
140141
$response = $this->client->send($request);
141142
} catch (\LogicException $e) {
142-
throw new ErrorException($e->getMessage());
143+
throw new ErrorException($e->getMessage(), $e->getCode(), $e);
144+
} catch (TwoFactorAuthenticationRequiredException $e) {
145+
throw $e;
143146
} catch (\RuntimeException $e) {
144-
throw new RuntimeException($e->getMessage());
147+
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
145148
}
146149

147150
$this->lastRequest = $request;

lib/Github/HttpClient/Listener/ErrorListener.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Github\HttpClient\Listener;
44

5+
use Github\Exception\TwoFactorAuthenticationRequiredException;
56
use Github\HttpClient\Message\ResponseMediator;
67
use Guzzle\Common\Event;
78
use Guzzle\Http\Message\Response;
@@ -45,6 +46,14 @@ public function onRequestError(Event $event)
4546
throw new ApiLimitExceedException($this->options['api_limit']);
4647
}
4748

49+
if (401 === $response->getStatusCode()) {
50+
if ($response->hasHeader('X-GitHub-OTP') && 0 === strpos((string) $response->getHeader('X-GitHub-OTP'), 'required;')) {
51+
$type = substr((string) $response->getHeader('X-GitHub-OTP'), 9);
52+
53+
throw new TwoFactorAuthenticationRequiredException($type);
54+
}
55+
}
56+
4857
$content = ResponseMediator::getContent($response);
4958
if (is_array($content) && isset($content['message'])) {
5059
if (400 == $response->getStatusCode()) {

test/Github/Tests/HttpClient/HttpClientTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,29 @@ public function shouldThrowExceptionWhenApiIsExceeded()
242242
$httpClient->get($path, $parameters, $headers);
243243
}
244244

245+
/**
246+
* @test
247+
* @expectedException \Github\Exception\TwoFactorAuthenticationRequiredException
248+
*/
249+
public function shouldForwardTwoFactorAuthenticationExceptionWhenItHappens()
250+
{
251+
$path = '/some/path';
252+
$parameters = array('a = b');
253+
$headers = array('c' => 'd');
254+
255+
$response = new Response(401);
256+
$response->addHeader('X-GitHub-OTP', 'required; sms');
257+
258+
$mockPlugin = new MockPlugin();
259+
$mockPlugin->addResponse($response);
260+
261+
$client = new GuzzleClient('http://123.com/');
262+
$client->addSubscriber($mockPlugin);
263+
264+
$httpClient = new TestHttpClient(array(), $client);
265+
$httpClient->get($path, $parameters, $headers);
266+
}
267+
245268
protected function getBrowserMock(array $methods = array())
246269
{
247270
$mock = $this->getMock(

test/Github/Tests/HttpClient/Listener/ErrorListenerTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,38 @@ public function shouldNotPassWhen422IsSentWithErrorCode($errorCode)
149149
$listener->onRequestError($this->getEventMock($response));
150150
}
151151

152+
/**
153+
* @test
154+
* @expectedException \Github\Exception\TwoFactorAuthenticationRequiredException
155+
*/
156+
public function shouldThrowTwoFactorAuthenticationRequiredException()
157+
{
158+
$response = $this->getMockBuilder('Guzzle\Http\Message\Response')->disableOriginalConstructor()->getMock();
159+
$response->expects($this->once())
160+
->method('isClientError')
161+
->will($this->returnValue(true));
162+
$response->expects($this->any())
163+
->method('getStatusCode')
164+
->will($this->returnValue(401));
165+
$response->expects($this->any())
166+
->method('getHeader')
167+
->will($this->returnCallback(function ($name) {
168+
switch ($name) {
169+
case 'X-RateLimit-Remaining':
170+
return 5000;
171+
case 'X-GitHub-OTP':
172+
return 'required; sms';
173+
}
174+
}));
175+
$response->expects($this->any())
176+
->method('hasHeader')
177+
->with('X-GitHub-OTP')
178+
->will($this->returnValue(true));
179+
180+
$listener = new ErrorListener(array());
181+
$listener->onRequestError($this->getEventMock($response));
182+
}
183+
152184
public function getErrorCodesProvider()
153185
{
154186
return array(

0 commit comments

Comments
 (0)