From dbb20bd05a6036f4df4cf4ccf1b34e9eea294247 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Tue, 4 Aug 2015 23:01:11 +0000 Subject: [PATCH] Support for JWT Bearer https://tools.ietf.org/html/draft-ietf-oauth-jwt-bearer-12 Please note, http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.5 refers to a URI which is defined as a URL or URN http://www.rfc-editor.org/rfc/rfc3305.txt --- lib/OAuth2.php | 25 ++++-- .../OAuth2GrantExtensionJwtBearer.php | 82 +++++++++++++++++++ tests/OAuth2Test.php | 42 ++++++++++ 3 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 tests/Fixtures/OAuth2GrantExtensionJwtBearer.php diff --git a/lib/OAuth2.php b/lib/OAuth2.php index d2a9b48..31da279 100644 --- a/lib/OAuth2.php +++ b/lib/OAuth2.php @@ -243,7 +243,7 @@ class OAuth2 * * @see http://tools.ietf.org/html/draft-ietf-oauth-v2-20#section-4.5 */ - const GRANT_TYPE_REGEXP = '#^(authorization_code|token|password|client_credentials|refresh_token|https?://.*)$#'; + const GRANT_TYPE_REGEXP = '#^(authorization_code|token|password|client_credentials|refresh_token|https?://.+|urn:.+)$#'; /** * @} @@ -823,12 +823,18 @@ public function grantAccessToken(Request $request = null) $stored = $this->grantAccessTokenRefreshToken($client, $input); break; default: - if (filter_var($input["grant_type"], FILTER_VALIDATE_URL)) { - // returns: true || array('scope' => scope) - $stored = $this->grantAccessTokenExtension($client, $inputData, $authHeaders); - } else { - throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_INVALID_REQUEST, 'Invalid grant_type parameter or parameter missing'); + if (substr($input["grant_type"], 0, 4) !== 'urn:' + && !filter_var($input["grant_type"], FILTER_VALIDATE_URL) + ) { + throw new OAuth2ServerException( + self::HTTP_BAD_REQUEST, + self::ERROR_INVALID_REQUEST, + 'Invalid grant_type parameter or parameter missing' + ); } + + // returns: true || array('scope' => scope) + $stored = $this->grantAccessTokenExtension($client, $inputData, $authHeaders); } if (!is_array($stored)) { @@ -1001,7 +1007,12 @@ protected function grantAccessTokenExtension(IOAuth2Client $client, array $input if (!($this->storage instanceof IOAuth2GrantExtension)) { throw new OAuth2ServerException(self::HTTP_BAD_REQUEST, self::ERROR_UNSUPPORTED_GRANT_TYPE); } - $uri = filter_var($inputData["grant_type"], FILTER_VALIDATE_URL); + + $uri = $inputData["grant_type"]; + if (substr($uri, 0, 4) !== 'urn:') { + $uri = filter_var($uri, FILTER_VALIDATE_URL); + } + $stored = $this->storage->checkGrantExtension($client, $uri, $inputData, $authHeaders); if ($stored === false) { diff --git a/tests/Fixtures/OAuth2GrantExtensionJwtBearer.php b/tests/Fixtures/OAuth2GrantExtensionJwtBearer.php new file mode 100644 index 0000000..06a4976 --- /dev/null +++ b/tests/Fixtures/OAuth2GrantExtensionJwtBearer.php @@ -0,0 +1,82 @@ +sub !== $decodedJwtStruct['sub']) { + return false; + } + + return array( + 'data' => $decodedJwtStruct, + ); + } + + public function setExpectedSubject($sub) + { + $this->sub = $sub; + } + + /** + * Let's pretend a JWT is endoded and signed by wrapping it in -ENCODED-JWT- + * + * In real life, we would verify the JWT is valid, and get the subject from it after decoding + * + * @param string An encoded JWT string + * @return array The decoded JWT struct + */ + public static function decodeJwt($encodedJwt) + { + $decodedJwt = str_replace('-ENCODED-JWT-', '', $encodedJwt); + return json_decode($decodedJwt, true); + } + + /** + * Let's pretend a JWT is endoded and signed by wrapping it in -ENCODED-JWT- + * + * In real life, we would verify the JWT is valid, and get the subject from it after decoding + * + * @param array A struct to encode as a JWT + * @return string The encoded JWT + */ + public static function encodeJwt($decodedStruct) + { + $decodedJwt = json_encode($decodedStruct); + $wrapper = '-ENCODED-JWT-'; + $encodedJwt = sprintf( + '%s%s%s', + $wrapper, + $decodedJwt, + $wrapper + ); + return $encodedJwt; + } +} diff --git a/tests/OAuth2Test.php b/tests/OAuth2Test.php index 7be2cb8..51c6d8b 100644 --- a/tests/OAuth2Test.php +++ b/tests/OAuth2Test.php @@ -684,6 +684,48 @@ public function testGrantAccessTokenWithGrantExtensionLimitedLifetime() $this->assertRegExp('{"access_token":"[^"]+","expires_in":86400,"token_type":"bearer"}', $response->getContent()); } + /** + * Tests OAuth2->grantAccessToken() with urn: extension + */ + public function testGrantAccessTokenWithGrantExtensionJwtBearer() + { + $clientId = 'cid'; + $clientSecret = 'csecret'; + $grantType = 'urn:ietf:params:oauth:grant-type:jwt-bearer'; + $subject = 1234; + + $stub = new \OAuth2\Tests\Fixtures\OAuth2GrantExtensionJwtBearer(); + $stub->addClient(new OAuth2Client($clientId, $clientSecret)); + $stub->setAllowedGrantTypes(array($grantType)); + $stub->setExpectedSubject($subject); + $oauth2 = new OAuth2($stub); + + $response = $oauth2->grantAccessToken(new Request(array( + 'grant_type' => $grantType, + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'jwt' => \OAuth2\Tests\Fixtures\OAuth2GrantExtensionJwtBearer::encodeJwt(array( + 'sub' => $subject, + )), + ))); + + $this->assertSame(array( + 'content-type' => array('application/json'), + 'cache-control' => array('no-store, private'), + 'pragma' => array('no-cache'), + ), array_diff_key( + $response->headers->all(), + array('date' => null) + )); + + $this->assertRegExp('{"access_token":"[^"]+","expires_in":3600,"token_type":"bearer","scope":null,"refresh_token":"[^"]+"}', $response->getContent()); + + $token = $stub->getLastAccessToken(); + $this->assertSame('cid', $token->getClientId()); + $data = $token->getData(); + $this->assertSame($subject, $data['sub']); + } + /** * Tests OAuth2->getAuthorizeParams()