@ -0,0 +1,119 @@ | |||
<?php | |||
use Symfony\Component\HttpFoundation\Request; | |||
use Symfony\Component\HttpFoundation\Response; | |||
class Token { | |||
public $http; | |||
private $_pretty = false; | |||
public function __construct() { | |||
$this->http = new p3k\HTTP(); | |||
} | |||
public function token(Request $request, Response $response) { | |||
if($request->get('pretty')) { | |||
$this->_pretty = true; | |||
} | |||
$source = $request->get('source'); | |||
$code = $request->get('code'); | |||
if(!$source) { | |||
return $this->respond($response, 400, [ | |||
'error' => 'invalid_request', | |||
'error_description' => 'Provide a source URL' | |||
]); | |||
} | |||
if(!$code) { | |||
return $this->respond($response, 400, [ | |||
'error' => 'invalid_request', | |||
'error_description' => 'Provide an authorization code' | |||
]); | |||
} | |||
$scheme = parse_url($source, PHP_URL_SCHEME); | |||
if(!in_array($scheme, ['http','https'])) { | |||
return $this->respond($response, 400, [ | |||
'error' => 'invalid_url', | |||
'error_description' => 'Only http and https URLs are supported' | |||
]); | |||
} | |||
// First try to discover the token endpoint | |||
$head = $this->http->head($source); | |||
if(!array_key_exists('Link', $head['headers'])) { | |||
return $this->respond($response, 200, [ | |||
'error' => 'no_token_endpoint', | |||
'error_description' => 'No Link headers were returned' | |||
]); | |||
} | |||
if(is_string($head['headers']['Link'])) | |||
$head['headers']['Link'] = [$head['headers']['Link']]; | |||
$rels = p3k\HTTP::link_rels($head['headers']); | |||
$endpoint = false; | |||
if(array_key_exists('token_endpoint', $rels)) { | |||
$endpoint = $rels['token_endpoint'][0]; | |||
} elseif(array_key_exists('oauth2-token', $rels)) { | |||
$endpoint = $rels['oauth2-token'][0]; | |||
} | |||
if(!$endpoint) { | |||
return $this->respond($response, 200, [ | |||
'error' => 'no_token_endpoint', | |||
'error_description' => 'No token endpoint was found in the headers' | |||
]); | |||
} | |||
// Resolve the endpoint URL relative to the source URL | |||
$endpoint = \mf2\resolveUrl($source, $endpoint); | |||
// Now exchange the code for a token | |||
$token = $this->http->post($endpoint, [ | |||
'grant_type' => 'authorization_code', | |||
'code' => $code | |||
]); | |||
// Catch HTTP errors here such as timeouts | |||
if($token['error']) { | |||
return $this->respond($response, 400, [ | |||
'error' => $token['error'], | |||
'error_description' => $token['error_description'] ?: 'An unknown error occurred trying to fetch the token' | |||
]); | |||
} | |||
// Otherwise pass through the response from the token endpoint | |||
$body = @json_decode($token['body']); | |||
// Pass through the content type if we were not able to decode the response as JSON | |||
$headers = []; | |||
if(!$body && isset($token['headers']['Content-Type'])) { | |||
$headers['Content-Type'] = $token['headers']['Content-Type']; | |||
} | |||
return $this->respond($response, $token['code'], $body ?: $token['body'], $headers); | |||
} | |||
private function respond(Response $response, $code, $params, $headers=[]) { | |||
$response->setStatusCode($code); | |||
foreach($headers as $k=>$v) { | |||
$response->headers->set($k, $v); | |||
} | |||
if(is_array($params) || is_object($params)) { | |||
$response->headers->set('Content-Type', 'application/json'); | |||
$opts = JSON_UNESCAPED_SLASHES; | |||
if($this->_pretty) $opts += JSON_PRETTY_PRINT; | |||
$response->setContent(json_encode($params, $opts)."\n"); | |||
} else { | |||
$response->setContent($params); | |||
} | |||
return $response; | |||
} | |||
} |
@ -0,0 +1,156 @@ | |||
<?php | |||
use Symfony\Component\HttpFoundation\Request; | |||
use Symfony\Component\HttpFoundation\Response; | |||
class TokenTest extends PHPUnit_Framework_TestCase { | |||
private $http; | |||
public function setUp() { | |||
$this->client = new Token(); | |||
$this->client->http = new p3k\HTTPTest(dirname(__FILE__).'/data/'); | |||
} | |||
private function token($params) { | |||
$request = new Request($params); | |||
$response = new Response(); | |||
return $this->client->token($request, $response); | |||
} | |||
public function testMissingURL() { | |||
$response = $this->token([]); | |||
$body = $response->getContent(); | |||
$this->assertEquals(400, $response->getStatusCode()); | |||
$data = json_decode($body); | |||
$this->assertObjectHasAttribute('error', $data); | |||
$this->assertEquals('invalid_request', $data->error); | |||
} | |||
public function testInvalidURL() { | |||
$url = 'ftp://example.com/foo'; | |||
$response = $this->token(['source' => $url, 'code' => '1234']); | |||
$body = $response->getContent(); | |||
$this->assertEquals(400, $response->getStatusCode()); | |||
$data = json_decode($body); | |||
$this->assertObjectHasAttribute('error', $data); | |||
$this->assertEquals('invalid_url', $data->error); | |||
} | |||
public function testMissingCode() { | |||
$response = $this->token(['source' => 'http://example.com/']); | |||
$body = $response->getContent(); | |||
$this->assertEquals(400, $response->getStatusCode()); | |||
$data = json_decode($body); | |||
$this->assertObjectHasAttribute('error', $data); | |||
$this->assertEquals('invalid_request', $data->error); | |||
} | |||
public function testNoLinkHeaders() { | |||
$url = 'http://private.example.com/no-link-headers'; | |||
$response = $this->token(['source' => $url, 'code' => '1234']); | |||
$body = $response->getContent(); | |||
$this->assertEquals(200, $response->getStatusCode()); | |||
$data = json_decode($body); | |||
$this->assertObjectHasAttribute('error', $data); | |||
$this->assertEquals('no_token_endpoint', $data->error); | |||
} | |||
public function testNoTokenEndpointOneLinkHeader() { | |||
$url = 'http://private.example.com/no-token-endpoint-one-link-header'; | |||
$response = $this->token(['source' => $url, 'code' => '1234']); | |||
$body = $response->getContent(); | |||
$this->assertEquals(200, $response->getStatusCode()); | |||
$data = json_decode($body); | |||
$this->assertObjectHasAttribute('error', $data); | |||
$this->assertEquals('no_token_endpoint', $data->error); | |||
} | |||
public function testNoTokenEndpointTwoLinkHeaders() { | |||
$url = 'http://private.example.com/no-token-endpoint-two-link-headers'; | |||
$response = $this->token(['source' => $url, 'code' => '1234']); | |||
$body = $response->getContent(); | |||
$this->assertEquals(200, $response->getStatusCode()); | |||
$data = json_decode($body); | |||
$this->assertObjectHasAttribute('error', $data); | |||
$this->assertEquals('no_token_endpoint', $data->error); | |||
} | |||
public function testTokenEndpointInOAuth2Rel() { | |||
$url = 'http://private.example.com/oauth2-token-endpoint'; | |||
$response = $this->token(['source' => $url, 'code' => '1234']); | |||
$body = $response->getContent(); | |||
$this->assertEquals(200, $response->getStatusCode()); | |||
$data = json_decode($body); | |||
$this->assertObjectNotHasAttribute('error', $data); | |||
$this->assertEquals('1234', $data->access_token); | |||
} | |||
public function testTokenEndpointInIndieAuthRel() { | |||
$url = 'http://private.example.com/token-endpoint'; | |||
$response = $this->token(['source' => $url, 'code' => '1234']); | |||
$body = $response->getContent(); | |||
$this->assertEquals(200, $response->getStatusCode()); | |||
$data = json_decode($body); | |||
$this->assertObjectNotHasAttribute('error', $data); | |||
$this->assertEquals('1234', $data->access_token); | |||
} | |||
public function testTokenEndpointWithMultipleRelLinks() { | |||
$url = 'http://private.example.com/multiple-rels'; | |||
$response = $this->token(['source' => $url, 'code' => '1234']); | |||
$body = $response->getContent(); | |||
$this->assertEquals(200, $response->getStatusCode()); | |||
$data = json_decode($body); | |||
$this->assertObjectNotHasAttribute('error', $data); | |||
$this->assertEquals('1234', $data->access_token); | |||
} | |||
public function testBadTokenEndpointResponse() { | |||
$url = 'http://private.example.com/token-endpoint-bad-response'; | |||
$response = $this->token(['source' => $url, 'code' => '1234']); | |||
$body = $response->getContent(); | |||
$this->assertEquals(400, $response->getStatusCode()); | |||
$data = json_decode($body); | |||
$this->assertObjectHasAttribute('error', $data); | |||
$this->assertEquals('this-string-passed-through-from-token-endpoint', $data->error); | |||
} | |||
public function testTokenEndpointTimeout() { | |||
$url = 'http://private.example.com/token-endpoint-timeout'; | |||
$response = $this->token(['source' => $url, 'code' => '1234']); | |||
$body = $response->getContent(); | |||
$this->assertEquals(400, $response->getStatusCode()); | |||
$data = json_decode($body); | |||
$this->assertObjectHasAttribute('error', $data); | |||
$this->assertEquals('timeout', $data->error); | |||
} | |||
public function testTokenEndpointReturnsNotJSON() { | |||
$url = 'http://private.example.com/token-endpoint-notjson'; | |||
$response = $this->token(['source' => $url, 'code' => '1234']); | |||
$body = $response->getContent(); | |||
$this->assertEquals(400, $response->getStatusCode()); | |||
$this->assertEquals('text/plain', $response->headers->get('content-type')); | |||
$this->assertEquals('Invalid request', $body); | |||
} | |||
} |
@ -0,0 +1,9 @@ | |||
HTTP/1.1 401 Unauthorized | |||
Server: Apache | |||
Date: Wed, 09 Dec 2015 03:29:14 GMT | |||
Content-Type: text/plain; charset=utf-8 | |||
Connection: keep-alive | |||
Link: </token>; rel="token_endpoint" | |||
Link: </webmention>; rel="webmention" | |||
This page uses the "token_endpoint" rel value defined in https://indieweb.org/obtaining-an-access-token |
@ -0,0 +1,7 @@ | |||
HTTP/1.1 401 Unauthorized | |||
Server: Apache | |||
Date: Wed, 09 Dec 2015 03:29:14 GMT | |||
Content-Type: text/plain; charset=utf-8 | |||
Connection: keep-alive | |||
This page has no link headers. |
@ -0,0 +1,8 @@ | |||
HTTP/1.1 401 Unauthorized | |||
Server: Apache | |||
Date: Wed, 09 Dec 2015 03:29:14 GMT | |||
Content-Type: text/plain; charset=utf-8 | |||
Connection: keep-alive | |||
Link: </micropub>; rel="micropub" | |||
This page has no token endpoint specified. |
@ -0,0 +1,9 @@ | |||
HTTP/1.1 401 Unauthorized | |||
Server: Apache | |||
Date: Wed, 09 Dec 2015 03:29:14 GMT | |||
Content-Type: text/plain; charset=utf-8 | |||
Connection: keep-alive | |||
Link: </micropub>; rel="micropub" | |||
Link: </webmention>; rel="webmention" | |||
This page has no token endpoint specified. |
@ -0,0 +1,8 @@ | |||
HTTP/1.1 401 Unauthorized | |||
Server: Apache | |||
Date: Wed, 09 Dec 2015 03:29:14 GMT | |||
Content-Type: text/plain; charset=utf-8 | |||
Connection: keep-alive | |||
Link: </token>; rel="oauth2-token" | |||
This page uses the "oauth2-token" rel value defined in https://tools.ietf.org/html/draft-wmills-oauth-lrdd-07 |
@ -0,0 +1,11 @@ | |||
HTTP/1.1 200 OK | |||
Server: Apache | |||
Date: Wed, 09 Dec 2015 03:29:14 GMT | |||
Content-Type: application/json | |||
Connection: keep-alive | |||
{ | |||
"access_token": "1234", | |||
"token_type": "bearer", | |||
"expires_in": 3600 | |||
} |
@ -0,0 +1,8 @@ | |||
HTTP/1.1 401 Unauthorized | |||
Server: Apache | |||
Date: Wed, 09 Dec 2015 03:29:14 GMT | |||
Content-Type: text/plain; charset=utf-8 | |||
Connection: keep-alive | |||
Link: </token>; rel="token_endpoint" | |||
This page uses the "token_endpoint" rel value defined in https://indieweb.org/obtaining-an-access-token |
@ -0,0 +1,8 @@ | |||
HTTP/1.1 401 Unauthorized | |||
Server: Apache | |||
Date: Wed, 09 Dec 2015 03:29:14 GMT | |||
Content-Type: text/plain; charset=utf-8 | |||
Connection: keep-alive | |||
Link: </token-invalid>; rel="token_endpoint" | |||
This page links to a token endpoint that will return a bad response |
@ -0,0 +1,8 @@ | |||
HTTP/1.1 401 Unauthorized | |||
Server: Apache | |||
Date: Wed, 09 Dec 2015 03:29:14 GMT | |||
Content-Type: text/plain; charset=utf-8 | |||
Connection: keep-alive | |||
Link: </token-notjson>; rel="token_endpoint" | |||
This page links to a token endpoint that does not return JSON |
@ -0,0 +1,8 @@ | |||
HTTP/1.1 401 Unauthorized | |||
Server: Apache | |||
Date: Wed, 09 Dec 2015 03:29:14 GMT | |||
Content-Type: text/plain; charset=utf-8 | |||
Connection: keep-alive | |||
Link: </token-timeout>; rel="token_endpoint" | |||
This page links to a token endpoint that will time out |
@ -0,0 +1,9 @@ | |||
HTTP/1.1 400 Bad Request | |||
Server: Apache | |||
Date: Wed, 09 Dec 2015 03:29:14 GMT | |||
Content-Type: application/json | |||
Connection: keep-alive | |||
{ | |||
"error": "this-string-passed-through-from-token-endpoint" | |||
} |
@ -0,0 +1,7 @@ | |||
HTTP/1.1 400 Bad Request | |||
Server: Apache | |||
Date: Wed, 09 Dec 2015 03:29:14 GMT | |||
Content-Type: text/plain | |||
Connection: keep-alive | |||
Invalid request |
@ -0,0 +1,4 @@ | |||
HTTP/1.1 400 Bad Request | |||
X-Test-Error: timeout | |||
. |