diff --git a/src/Provider/AbstractProvider.php b/src/Provider/AbstractProvider.php index d544ca9b..3472bf34 100644 --- a/src/Provider/AbstractProvider.php +++ b/src/Provider/AbstractProvider.php @@ -16,64 +16,44 @@ abstract class AbstractProvider implements ProviderInterface { /** - * @var string + * @var string HTTP method used to fetch access tokens. */ - public $clientId = ''; + const ACCESS_TOKEN_METHOD = 'post'; /** - * @var string + * @var string Key used in the access token response to identify the user. */ - public $clientSecret = ''; + const ACCESS_TOKEN_UID = 'uid'; /** - * @var string + * @var string Type of response expected from the provider. */ - public $redirectUri = ''; + const RESPONSE_TYPE = 'json'; /** - * @var string + * @var string Separator used for authorization scopes. */ - public $state; + const SCOPE_SEPARATOR = ','; /** * @var string */ - public $name; + protected $clientId; /** * @var string */ - public $uidKey = 'uid'; - - /** - * @var array - */ - public $scopes = []; + protected $clientSecret; /** * @var string */ - public $method = 'post'; + protected $redirectUri; /** * @var string */ - public $scopeSeparator = ','; - - /** - * @var string - */ - public $responseType = 'json'; - - /** - * @var array - */ - public $headers = []; - - /** - * @var string - */ - public $authorizationHeader; + protected $state; /** * @var GrantFactory @@ -90,11 +70,6 @@ abstract class AbstractProvider implements ProviderInterface */ protected $randomFactory; - /** - * @var Closure - */ - protected $redirectHandler; - /** * @var int This represents: PHP_QUERY_RFC1738, which is the default value for php 5.4 * and the default encryption type for the http_build_query setup @@ -129,6 +104,12 @@ public function __construct($options = [], array $collaborators = []) $this->setRandomFactory($collaborators['randomFactory']); } + /** + * Set the grant factory instance. + * + * @param GrantFactory $factory + * @return $this + */ public function setGrantFactory(GrantFactory $factory) { $this->grantFactory = $factory; @@ -136,13 +117,22 @@ public function setGrantFactory(GrantFactory $factory) return $this; } + /** + * Get the grant factory instance. + * + * @return GrantFactory + */ public function getGrantFactory() { - $factory = $this->grantFactory; - - return $factory; + return $this->grantFactory; } + /** + * Set the HTTP adapter instance. + * + * @param HttpAdapterInterface $client + * @return $this + */ public function setHttpClient(HttpAdapterInterface $client) { $this->httpClient = $client; @@ -150,11 +140,14 @@ public function setHttpClient(HttpAdapterInterface $client) return $this; } + /** + * Get the HTTP adapter instance. + * + * @return HttpAdapterInterface + */ public function getHttpClient() { - $client = $this->httpClient; - - return $client; + return $this->httpClient; } /** @@ -180,6 +173,18 @@ public function getRandomFactory() return $this->randomFactory; } + /** + * Get the current state of the OAuth flow. + * + * This can be accessed by the redirect handler during authorization. + * + * @return string + */ + public function getState() + { + return $this->state; + } + // Implementing these interfaces methods should not be required, but not // doing so will break HHVM because of https://github.com/facebook/hhvm/issues/5170 // Once HHVM is working, delete the following abstract methods. @@ -188,16 +193,6 @@ abstract public function urlAccessToken(); abstract public function urlUserDetails(AccessToken $token); // End of methods to delete. - public function getScopes() - { - return $this->scopes; - } - - public function setScopes(array $scopes) - { - $this->scopes = $scopes; - } - /** * Get a new random string to use for auth state. * @@ -213,27 +208,37 @@ protected function getRandomState($length = 32) return $generator->generateString($length); } + /** + * Get the default scopes used by this provider. + * + * This should not be a complete list of all scopes, but the minimum + * required for the provider user interface! + * + * @return array + */ + abstract protected function getDefaultScopes(); + public function getAuthorizationUrl(array $options = []) { if (empty($options['state'])) { $options['state'] = $this->getRandomState(); } - - // Store the state, it may need to be accessed later. - $this->state = $options['state']; + if (empty($options['scope'])) { + $options['scope'] = $this->getDefaultScopes(); + } $options += [ - // Do not set the default state here! The random generator takes a - // non-trivial amount of time to run. 'response_type' => 'code', 'approval_prompt' => 'auto', - 'scope' => $this->scopes, ]; if (is_array($options['scope'])) { - $options['scope'] = implode($this->scopeSeparator, $options['scope']); + $options['scope'] = implode(static::SCOPE_SEPARATOR, $options['scope']); } + // Store the state, it may need to be accessed later. + $this->state = $options['state']; + $params = [ 'client_id' => $this->clientId, 'redirect_uri' => $this->redirectUri, @@ -246,13 +251,11 @@ public function getAuthorizationUrl(array $options = []) return $this->urlAuthorize().'?'.$this->httpBuildQuery($params, '', '&'); } - // @codeCoverageIgnoreStart - public function authorize(array $options = []) + public function authorize(array $options = [], $redirectHandler = null) { $url = $this->getAuthorizationUrl($options); - if ($this->redirectHandler) { - $handler = $this->redirectHandler; - return $handler($url, $this); + if ($redirectHandler) { + return $redirectHandler($url, $this); } // @codeCoverageIgnoreStart header('Location: ' . $url); @@ -279,7 +282,7 @@ public function getAccessToken($grant = 'authorization_code', array $params = [] try { $client = $this->getHttpClient(); - switch (strtoupper($this->method)) { + switch (strtoupper(static::ACCESS_TOKEN_METHOD)) { case 'GET': // @codeCoverageIgnoreStart // No providers included with this library use get but 3rd parties may @@ -372,7 +375,7 @@ protected function parseResponse($response) { $result = []; - switch ($this->responseType) { + switch (static::RESPONSE_TYPE) { case 'json': $result = json_decode($response, true); if (JSON_ERROR_NONE !== json_last_error()) { @@ -399,36 +402,20 @@ protected function parseResponse($response) abstract protected function checkResponse(array $response); /** - * Prepare the access token response for the grant. Custom mapping of - * expirations, etc should be done here. + * Prepare the access token response for the grant. + * + * Custom mapping of expirations, etc should be done here. Always call the + * parent method when overloading this method! * * @param array $result * @return array */ protected function prepareAccessTokenResult(array $result) { - $this->setResultUid($result); + $result['uid'] = $result[static::ACCESS_TOKEN_UID]; return $result; } - /** - * Sets any result keys we've received matching our provider-defined uidKey to the key "uid". - * - * @param array $result - */ - protected function setResultUid(array &$result) - { - // If we're operating with the default uidKey there's nothing to do. - if ($this->uidKey === "uid") { - return; - } - - if (isset($result[$this->uidKey])) { - // The AccessToken expects a "uid" to have the key "uid". - $result['uid'] = $result[$this->uidKey]; - } - } - /** * Generate a user object from a successful user details request. * @@ -479,26 +466,49 @@ protected function fetchUserDetails(AccessToken $token) return $this->getResponse($request); } - protected function getAuthorizationHeaders($token) + /** + * Get additional headers used by this provider. + * + * Typically this is used to set Accept or Content-Type headers. + * + * @param AccessToken $token + * @return array + */ + protected function getDefaultHeaders($token = null) { - $headers = []; - if ($this->authorizationHeader) { - $headers['Authorization'] = $this->authorizationHeader . ' ' . $token; - } - return $headers; + return []; + } + + /** + * Get authorization headers used by this provider. + * + * Typically this is "Bearer" or "MAC". For more information see: + * http://tools.ietf.org/html/rfc6749#section-7.1 + * + * No default is provided, providers must overload this method to activate + * authorization headers. + * + * @return array + */ + protected function getAuthorizationHeaders($token = null) + { + return []; } + /** + * Get the headers used by this provider for a request. + * + * If a token is passed, the request may be authenticated through headers. + * + * @param mixed $token object or string + * @return array + */ public function getHeaders($token = null) { - $headers = $this->headers; + $headers = $this->getDefaultHeaders(); if ($token) { $headers = array_merge($headers, $this->getAuthorizationHeaders($token)); } return $headers; } - - public function setRedirectHandler(Closure $handler) - { - $this->redirectHandler = $handler; - } } diff --git a/src/Provider/ProviderInterface.php b/src/Provider/ProviderInterface.php index 3ea46e5e..5ed23b54 100644 --- a/src/Provider/ProviderInterface.php +++ b/src/Provider/ProviderInterface.php @@ -32,21 +32,6 @@ public function urlAccessToken(); */ public function urlUserDetails(AccessToken $token); - /** - * Get the configured scopes for this provider. - * - * @return array - */ - public function getScopes(); - - /** - * Configure the scopes that will be requested by this provider. - * - * @param array $scopes - * @return void - */ - public function setScopes(array $scopes); - /** * Get the URL that this provider uses to request authorization. * diff --git a/src/Tool/BearerAuthorizationTrait.php b/src/Tool/BearerAuthorizationTrait.php new file mode 100644 index 00000000..1a0e2bf7 --- /dev/null +++ b/src/Tool/BearerAuthorizationTrait.php @@ -0,0 +1,16 @@ + 'Bearer ' . $token]; + } +} diff --git a/src/Tool/MacAuthorizationTrait.php b/src/Tool/MacAuthorizationTrait.php new file mode 100644 index 00000000..6acb2340 --- /dev/null +++ b/src/Tool/MacAuthorizationTrait.php @@ -0,0 +1,55 @@ +getTokenId($token); + $nonce = $this->getRandomState(16); + $mac = $this->getMacSignature($id, $ts, $nonce); + + $parts = []; + foreach (compact('id', 'ts', 'nonce', 'mac') as $key => $value) { + $parts[] = sprintf('%s="%s"', $key, $value); + } + + return ['Authorization' => 'MAC ' . implode(",\n", $parts)]; + // @codeCoverageIgnoreEnd + } +} diff --git a/test/src/Provider/AbstractProviderTest.php b/test/src/Provider/AbstractProviderTest.php index 41740182..d179fbdd 100644 --- a/test/src/Provider/AbstractProviderTest.php +++ b/test/src/Provider/AbstractProviderTest.php @@ -64,21 +64,13 @@ public function testConstructorSetsProperties() 'clientId' => '1234', 'clientSecret' => '4567', 'redirectUri' => 'http://example.org/redirect', - 'state' => 'foo', - 'name' => 'bar', - 'uidKey' => 'mynewuid', - 'scopes' => ['a', 'b', 'c'], - 'method' => 'get', - 'scopeSeparator' => ';', - 'responseType' => 'csv', - 'headers' => ['Foo' => 'Bar'], - 'authorizationHeader' => 'Bearer', + 'httpBuildEncType' => 4, ]; $mockProvider = new MockProvider($options); foreach ($options as $key => $value) { - $this->assertEquals($value, $mockProvider->{$key}); + $this->assertAttributeEquals($value, $key, $mockProvider); } } @@ -113,15 +105,13 @@ public function testSetRedirectHandler() $callback = function ($url, $provider) { $this->testFunction = $url; - $this->state = $provider->state; + $this->state = $provider->getState(); }; - $this->provider->setRedirectHandler($callback); - - $this->provider->authorize(); + $this->provider->authorize([], $callback); $this->assertNotFalse($this->testFunction); - $this->assertEquals($this->provider->state, $this->state); + $this->assertAttributeEquals($this->state, 'state', $this->provider); } /** @@ -215,6 +205,13 @@ public function getHeadersTest() public function testScopesOverloadedDuringAuthorize() { + $url = $this->provider->getAuthorizationUrl(); + + parse_str(parse_url($url, PHP_URL_QUERY), $qs); + + $this->assertArrayHasKey('scope', $qs); + $this->assertSame('test', $qs['scope']); + $url = $this->provider->getAuthorizationUrl(['scope' => ['foo', 'bar']]); parse_str(parse_url($url, PHP_URL_QUERY), $qs); @@ -259,7 +256,7 @@ public function testGetAccessToken() ]); $grant_name = 'mock'; - $raw_response = ['access_token' => 'okay', 'expires_in' => 3600]; + $raw_response = ['access_token' => 'okay', 'expires' => time() + 3600, 'uid' => 3]; $token = new AccessToken($raw_response); $contains_correct_grant_type = function ($params) use ($grant_name) { @@ -299,6 +296,9 @@ public function testGetAccessToken() $result = $provider->getAccessToken($grant, ['code' => 'mock_authorization_code']); $this->assertSame($result, $token); + $this->assertSame($raw_response['uid'], $token->uid); + $this->assertSame($raw_response['access_token'], $token->accessToken); + $this->assertSame($raw_response['expires'], $token->expires); } public function testErrorResponsesCanBeCustomizedAtTheProvider() @@ -403,8 +403,7 @@ public function testAuthenticatedRequestAndResponse() { $token = new AccessToken(['access_token' => 'abc', 'expires_in' => 3600]); - $provider = clone $this->provider; - $provider->authorizationHeader = 'Bearer'; + $provider = new MockProvider(['authorizationHeader' => 'Bearer']); $request = $provider->getAuthenticatedRequest('get', 'https://api.example.com/v1/test', $token); $this->assertInstanceOf('Ivory\HttpAdapter\Message\RequestInterface', $request); diff --git a/test/src/Provider/Fake.php b/test/src/Provider/Fake.php index 7eb837a0..32601e31 100644 --- a/test/src/Provider/Fake.php +++ b/test/src/Provider/Fake.php @@ -3,11 +3,14 @@ namespace League\OAuth2\Client\Test\Provider; use League\OAuth2\Client\Token\AccessToken; +use League\OAuth2\Client\Tool\BearerAuthorizationTrait; use League\OAuth2\Client\Provider\AbstractProvider; use League\OAuth2\Client\Provider\Exception\IdentityProviderException; class Fake extends AbstractProvider { + use BearerAuthorizationTrait; + public function urlAuthorize() { return 'http://example.com/oauth/authorize'; @@ -23,6 +26,11 @@ public function urlUserDetails(AccessToken $token) return 'http://example.com/oauth/user'; } + protected function getDefaultScopes() + { + return ['test']; + } + protected function prepareUserDetails(array $response, AccessToken $token) { return new Fake\User($response);