From bdfc1088fd1ce99785817bb89bccb46b1a1c7384 Mon Sep 17 00:00:00 2001 From: steffkes Date: Thu, 7 May 2015 16:17:15 +0200 Subject: [PATCH 1/5] Add GrantType for JSON Web Token (JWT) --- composer.json | 3 +- composer.lock | 49 +++++++++++++++++++++- src/GrantType/GrantTypeBase.php | 14 +++++++ src/GrantType/JwtBearer.php | 70 +++++++++++++++++++++++++++++++ tests/GrantType/JwtBearerTest.php | 27 ++++++++++++ tests/MockOAuth2Server.php | 16 +++++++ tests/private.key | 28 +++++++++++++ 7 files changed, 204 insertions(+), 3 deletions(-) create mode 100755 src/GrantType/JwtBearer.php create mode 100644 tests/GrantType/JwtBearerTest.php create mode 100755 tests/private.key diff --git a/composer.json b/composer.json index 56fb928..4d9e44c 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,8 @@ "description": "An OAuth2 plugin (subscriber) for Guzzle", "license": "MIT", "require": { - "guzzlehttp/guzzle": "~5.0" + "guzzlehttp/guzzle": "~5.0", + "firebase/php-jwt": "^2.0" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index b0faf19..5347c45 100644 --- a/composer.lock +++ b/composer.lock @@ -1,11 +1,56 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "aa6a34af010b810e5fb3a7e678a89cbe", + "hash": "85d16e854bdd3f0778c56ff75c582437", "packages": [ + { + "name": "firebase/php-jwt", + "version": "2.0.0", + "target-dir": "Firebase/PHP-JWT", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "ffcfd888ce1e4f2d70cac2dc9b7301038332fe57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/ffcfd888ce1e4f2d70cac2dc9b7301038332fe57", + "reference": "ffcfd888ce1e4f2d70cac2dc9b7301038332fe57", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "Authentication/", + "Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "time": "2015-04-01 18:46:38" + }, { "name": "guzzlehttp/guzzle", "version": "5.2.0", diff --git a/src/GrantType/GrantTypeBase.php b/src/GrantType/GrantTypeBase.php index eba84ca..360a854 100644 --- a/src/GrantType/GrantTypeBase.php +++ b/src/GrantType/GrantTypeBase.php @@ -52,6 +52,16 @@ protected function getRequired() return ['client_id']; } + /** + * Get additional options, if any. + * + * @return array|null + */ + protected function getAdditionalOptions() + { + return null; + } + /** * @inheritdoc */ @@ -72,6 +82,10 @@ public function getToken() $requestOptions['body'] = $body; + if ($additionalOptions = $this->getAdditionalOptions()) { + $requestOptions = array_merge_recursive($requestOptions, $additionalOptions); + } + $response = $this->client->post($config['token_url'], $requestOptions); $data = $response->json(); diff --git a/src/GrantType/JwtBearer.php b/src/GrantType/JwtBearer.php new file mode 100755 index 0000000..777e55a --- /dev/null +++ b/src/GrantType/JwtBearer.php @@ -0,0 +1,70 @@ +config->get('private_key') instanceof SplFileObject)) { + throw new InvalidArgumentException('private_key needs to be instance of SplFileObject'); + } + } + + /** + * @inheritdoc + */ + protected function getRequired() + { + return array_merge(parent::getRequired(), ['private_key']); + } + + /** + * @inheritdoc + */ + protected function getAdditionalOptions() + { + return [ + 'body' => [ + 'assertion' => $this->computeJwt() + ] + ]; + } + + /** + * Compute JWT, signing with provided private key + */ + protected function computeJwt() + { + $keyFile = $this->config->get('private_key'); + $key = $keyFile->fread($keyFile->getSize()); + + $payload = [ + 'iss' => $this->config->get('client_id'), + 'aud' => sprintf('%s/%s', rtrim($this->client->getBaseUrl(), '/'), ltrim($this->config->get('token_url'), '/')), + 'exp' => time() + 60 * 60 * 1000, + 'iat' => time() + ]; + + return JWT::encode($payload, $key, 'RS256'); + } +} diff --git a/tests/GrantType/JwtBearerTest.php b/tests/GrantType/JwtBearerTest.php new file mode 100644 index 0000000..f4cd183 --- /dev/null +++ b/tests/GrantType/JwtBearerTest.php @@ -0,0 +1,27 @@ +setExpectedException('\\InvalidArgumentException', 'Config is missing the following keys: client_id, private_key'); + new JwtBearer($this->getClient()); + } + + public function testValidRequestGetsToken() + { + $grantType = new JwtBearer($this->getClient(), [ + 'client_id' => 'testClient', + 'private_key' => new SplFileObject(__DIR__.'/../private.key') + ]); + $token = $grantType->getToken(); + $this->assertNotEmpty($token->getToken()); + $this->assertTrue($token->getExpires()->getTimestamp() > time()); + } +} diff --git a/tests/MockOAuth2Server.php b/tests/MockOAuth2Server.php index f300da4..e1abcce 100644 --- a/tests/MockOAuth2Server.php +++ b/tests/MockOAuth2Server.php @@ -68,6 +68,8 @@ protected function oauth2Token(array $request) case 'refresh_token': return $this->grantTypeRefreshToken($requestBody); + case 'urn:ietf:params:oauth:grant-type:jwt-bearer': + return $this->grantTypeJwtBearer($requestBody); } throw new \RuntimeException("Test grant type not implemented: $grantType"); } @@ -140,6 +142,20 @@ protected function grantTypeRefreshToken(array $requestBody) return $this->validTokenResponse(); } + /** + * @param array $requestBody + * + * @return array + */ + protected function grantTypeJwtBearer(array $requestBody) + { + if (!array_key_exists('assertion', $requestBody)) { + return ['status' => 401]; + } + + return $this->validTokenResponse(); + } + /** * @param array $request * diff --git a/tests/private.key b/tests/private.key new file mode 100755 index 0000000..c117308 --- /dev/null +++ b/tests/private.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCTT//DLGGM1jEJ +kBPc69qjiS07nsYXwsawFChRBGu03MCUcyBr7wz6oq1jkCyJ0ZHAcTmY6bpdCpSf +JM3dt16/GRvz+edmYB/CxnnIGQmlmH+kGAAQkA1wtkZLhYYRbpdb/aDE0Azx6M8h +ibe+dOidlJJQhg1SSwPcVUVWpqs4atelf9QeQtrD92igUZhwpHjQgAE4WCrXPpaO +x/07QiUge0pWHpFBamHwoalu3JLmWEED2uiAunKiY9+VJi6GAJABzIHbsh6i1XU3 +ATF2WNo7qgY2e8XlDanQypvq93qq+erwynclNJh+5yVhDCqD+IX0/knGb2MMdHTP +XiU8pLjPAgMBAAECggEAJMl/h0/X9IGwsUCnlS3Y5anl/9OAiIJ9d48xGjpOY1YV +SX0OhaWmyhhB0HE6jhglm7cquQL1JTL1NmDMgCfAo1wz3NN1c91hURSbaNrHy/Cv +P1029uviT1lVaJqphkTly3Uk5sFF2ktXHnrzxb4QMPnfJ/ix7vEIv8cTj7YDYAz9 +WzdPk3dZ9HyWtt+hVc1fJSNdaqyNQyRzgiUuMCIu37j/MQ5mPWucm4siZcSzFfhl +yp8JZX0GoKtNa1zCroyDGpOFt5iVUqInQhECbfW05YFW+1KZaGInqT22hHu4TDAg ++2iiLdQnDf/ubNouMnw+wV5w922MowHGfcy2GSwaMQKBgQDDFGSVDP0KRhtfsdsk +Km9nxQTJrxwL9K69vUGsr+xraC99BmXl+IrJRP9hiYl8YC1fhkQzB6h7LfG8KGyE +V+FQQNSHuRMXtK76PU4cEqv8h0HBzxRix93dctAw8bYIfawqgPG+r3MLhBMMYy5O +Pwms6YRZcDv3SKr8/CohdZVp9QKBgQDBUN6C0TgW8lKdemRuXtEpKDlCxDzXV6nt +jPhHQ3gHVoDQkGyArZyFsa0DJmD/BOCVjCd+H2cO7EQeIJOHRxF9RerdRgfIt8lJ +MtIUH3Ehep8R3oqSFneoZkdBjNH/tXpYTGhKfPpBZPYA0++lEr72nVYs78SMUrgE +7JZX1ysJMwKBgAqvpUrc6UeUy48UaRK0GGIw0rBRnVGyV5ghM+XHxUWk8WUB4rcU +RFX+J5cqN5POmO2wpy+8bahBvgo2lKszPS5uPrYolzknNqaSkSLMiwtMRXfeZhl7 +JVYqIelsdDJG4BV79sIhTkYFOB3nmPPEVD1alVto4IANRQCSt6QZktO5AoGAcpN2 +vjw4rUkEdDfFbLEf8O/ZOFxM3ykjGxuRT9OKQXcgs/zVglLj0U2kiJhnpt6CKcC+ +6367O1oHaX/PUL9rez9EW8+U738Wex726lxUVg5yV0n6AWn1k8bC9vP6xz8Ne2YV +7ggy3y1yrLzwbXs12b8ZA1s8uBqS3MBIv1lVNYcCgYBYNpWtAWC5bIF/TgtrShrr +Fw52T+W3Bf36codGT+E3WYlQiONDWt2H8Bd9EO1j7bVP5wk0CYdnTOKqSQTnxo0r +THS6qHcin1XW566t09dLvoxCwRCSPsP8V3oZB9W31zndZNUhnxVT+RXNUu6i3XXz +JTn6lp0rp9eHJEFV/s0Zkg== +-----END PRIVATE KEY----- \ No newline at end of file From de531c25529174d28f974a384edd9e57fb6113c5 Mon Sep 17 00:00:00 2001 From: steffkes Date: Thu, 21 May 2015 17:42:59 +0200 Subject: [PATCH 2/5] Ensure reading SplFileObject is compatible to php-5.4 --- src/GrantType/JwtBearer.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/GrantType/JwtBearer.php b/src/GrantType/JwtBearer.php index 777e55a..3fb3345 100755 --- a/src/GrantType/JwtBearer.php +++ b/src/GrantType/JwtBearer.php @@ -55,9 +55,6 @@ protected function getAdditionalOptions() */ protected function computeJwt() { - $keyFile = $this->config->get('private_key'); - $key = $keyFile->fread($keyFile->getSize()); - $payload = [ 'iss' => $this->config->get('client_id'), 'aud' => sprintf('%s/%s', rtrim($this->client->getBaseUrl(), '/'), ltrim($this->config->get('token_url'), '/')), @@ -65,6 +62,20 @@ protected function computeJwt() 'iat' => time() ]; - return JWT::encode($payload, $key, 'RS256'); + return JWT::encode($payload, $this->readPrivateKey($this->config->get('private_key')), 'RS256'); + } + + /** + * Read private key + * + * @param SplFileObject $privateKey + */ + protected function readPrivateKey(SplFileObject $privateKey) + { + $key = ''; + while (!$privateKey->eof()) { + $key .= $privateKey->fgets(); + } + return $key; } } From 68035ff048f9346cf18a0d7fdbbcb8e30a35d555 Mon Sep 17 00:00:00 2001 From: steffkes Date: Thu, 21 May 2015 18:17:07 +0200 Subject: [PATCH 3/5] Proper indentation makes scrutinizer happy --- src/GrantType/JwtBearer.php | 38 +++++++++++++++---------------- tests/GrantType/JwtBearerTest.php | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/GrantType/JwtBearer.php b/src/GrantType/JwtBearer.php index 3fb3345..30eec1c 100755 --- a/src/GrantType/JwtBearer.php +++ b/src/GrantType/JwtBearer.php @@ -23,11 +23,11 @@ class JwtBearer extends GrantTypeBase */ public function __construct(ClientInterface $client, array $config = []) { - parent::__construct($client, $config); + parent::__construct($client, $config); - if (!($this->config->get('private_key') instanceof SplFileObject)) { - throw new InvalidArgumentException('private_key needs to be instance of SplFileObject'); - } + if (!($this->config->get('private_key') instanceof SplFileObject)) { + throw new InvalidArgumentException('private_key needs to be instance of SplFileObject'); + } } /** @@ -44,9 +44,9 @@ protected function getRequired() protected function getAdditionalOptions() { return [ - 'body' => [ - 'assertion' => $this->computeJwt() - ] + 'body' => [ + 'assertion' => $this->computeJwt() + ] ]; } @@ -55,14 +55,14 @@ protected function getAdditionalOptions() */ protected function computeJwt() { - $payload = [ - 'iss' => $this->config->get('client_id'), - 'aud' => sprintf('%s/%s', rtrim($this->client->getBaseUrl(), '/'), ltrim($this->config->get('token_url'), '/')), - 'exp' => time() + 60 * 60 * 1000, - 'iat' => time() - ]; + $payload = [ + 'iss' => $this->config->get('client_id'), + 'aud' => sprintf('%s/%s', rtrim($this->client->getBaseUrl(), '/'), ltrim($this->config->get('token_url'), '/')), + 'exp' => time() + 60 * 60 * 1000, + 'iat' => time() + ]; - return JWT::encode($payload, $this->readPrivateKey($this->config->get('private_key')), 'RS256'); + return JWT::encode($payload, $this->readPrivateKey($this->config->get('private_key')), 'RS256'); } /** @@ -72,10 +72,10 @@ protected function computeJwt() */ protected function readPrivateKey(SplFileObject $privateKey) { - $key = ''; - while (!$privateKey->eof()) { - $key .= $privateKey->fgets(); - } - return $key; + $key = ''; + while (!$privateKey->eof()) { + $key .= $privateKey->fgets(); + } + return $key; } } diff --git a/tests/GrantType/JwtBearerTest.php b/tests/GrantType/JwtBearerTest.php index f4cd183..3fe5384 100644 --- a/tests/GrantType/JwtBearerTest.php +++ b/tests/GrantType/JwtBearerTest.php @@ -18,7 +18,7 @@ public function testValidRequestGetsToken() { $grantType = new JwtBearer($this->getClient(), [ 'client_id' => 'testClient', - 'private_key' => new SplFileObject(__DIR__.'/../private.key') + 'private_key' => new SplFileObject(__DIR__ . '/../private.key') ]); $token = $grantType->getToken(); $this->assertNotEmpty($token->getToken()); From 43df82b42ff95d9cabbd8b1e6a0e5d87c97c7ec5 Mon Sep 17 00:00:00 2001 From: steffkes Date: Thu, 21 May 2015 18:28:49 +0200 Subject: [PATCH 4/5] test for invalid type ofprivate_key instance --- tests/GrantType/JwtBearerTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/GrantType/JwtBearerTest.php b/tests/GrantType/JwtBearerTest.php index 3fe5384..7ca3e96 100644 --- a/tests/GrantType/JwtBearerTest.php +++ b/tests/GrantType/JwtBearerTest.php @@ -14,6 +14,15 @@ public function testMissingConfigException() new JwtBearer($this->getClient()); } + public function testPrivateKeyNotSplFileObject() + { + $this->setExpectedException('\\InvalidArgumentException', 'private_key needs to be instance of SplFileObject'); + $grantType = new JwtBearer($this->getClient(), [ + 'client_id' => 'testClient', + 'private_key' => 'INVALID' + ]); + } + public function testValidRequestGetsToken() { $grantType = new JwtBearer($this->getClient(), [ From b900be853c1c485f328eceff8ab821607813396d Mon Sep 17 00:00:00 2001 From: steffkes Date: Tue, 26 May 2015 19:34:19 +0200 Subject: [PATCH 5/5] it's PHP .. not JS! --- src/GrantType/JwtBearer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GrantType/JwtBearer.php b/src/GrantType/JwtBearer.php index 30eec1c..682083a 100755 --- a/src/GrantType/JwtBearer.php +++ b/src/GrantType/JwtBearer.php @@ -58,7 +58,7 @@ protected function computeJwt() $payload = [ 'iss' => $this->config->get('client_id'), 'aud' => sprintf('%s/%s', rtrim($this->client->getBaseUrl(), '/'), ltrim($this->config->get('token_url'), '/')), - 'exp' => time() + 60 * 60 * 1000, + 'exp' => time() + 60 * 60, 'iat' => time() ];