Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor to JWT library #441

Merged
merged 3 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"psr/container": "^1.0 | ^2.0",
"psr/http-client-implementation": "^1.0",
"vonage/nexmo-bridge": "^0.1.0",
"psr/log": "^1.1|^2.0|^3.0"
"psr/log": "^1.1|^2.0|^3.0",
"vonage/jwt": "^0.4.0"
},
"require-dev": {
"guzzlehttp/guzzle": ">=6",
Expand Down
70 changes: 31 additions & 39 deletions src/Client/Credentials/Keypair.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
namespace Vonage\Client\Credentials;

use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token;
use Vonage\Application\Application;
use Vonage\Client\Exception\Validation;
use Vonage\JWT\TokenGenerator;

use function base64_encode;
use function mt_rand;
Expand All @@ -27,14 +30,9 @@
*/
class Keypair extends AbstractCredentials
{
/**
* @var Key
*/
protected $key;

public function __construct($privateKey, $application = null)
public function __construct(protected string $key, $application = null)
{
$this->credentials['key'] = $privateKey;
$this->credentials['key'] = $key;

if ($application) {
if ($application instanceof Application) {
Expand All @@ -43,71 +41,65 @@ public function __construct($privateKey, $application = null)

$this->credentials['application'] = $application;
}

$this->key = InMemory::plainText($privateKey);
}

/**
* @return Key
* @deprecated Old public signature using Lcobucci/Jwt directly
*/
public function getKey(): Key
{
return InMemory::plainText($this->key);
}

public function getKeyRaw(): string
{
return $this->key;
}

public function generateJwt(array $claims = []): Token
{
$config = Configuration::forSymmetricSigner(new Sha256(), $this->key);

$exp = time() + 60;
$iat = time();
$jti = base64_encode((string)mt_rand());
$generator = new TokenGenerator($this->application, $this->getKeyRaw());

if (isset($claims['exp'])) {
$exp = $claims['exp'];

unset($claims['exp']);
// This will change to an Exception in 5.0
trigger_error('Expiry date is automatically generated from now and TTL, so cannot be passed in
as an argument in claims', E_USER_WARNING);
unset($claims['nbf']);
}

if (isset($claims['iat'])) {
$iat = $claims['iat'];

unset($claims['iat']);
if (isset($claims['ttl'])) {
$generator->setTTL($claims['ttl']);
unset($claims['ttl']);
}

if (isset($claims['jti'])) {
$jti = $claims['jti'];

$generator->setJTI($claims['jti']);
unset($claims['jti']);
}

$builder = $config->builder();
$builder->issuedAt((new \DateTimeImmutable())->setTimestamp($iat))
->expiresAt((new \DateTimeImmutable())->setTimestamp($exp))
->identifiedBy($jti);

if (isset($claims['nbf'])) {
$builder->canOnlyBeUsedAfter((new \DateTimeImmutable())->setTimestamp($claims['nbf']));

// Due to older versions of lcobucci/jwt, this claim has
// historic fraction conversation issues. For now, nbf is not supported.
// This will change to an Exception in 5.0
trigger_error('NotBefore Claim is not supported in Vonage JWT', E_USER_WARNING);
unset($claims['nbf']);
}

if (isset($this->credentials['application'])) {
$builder->withClaim('application_id', $this->credentials['application']);
}

if (isset($claims['sub'])) {
$builder->relatedTo($claims['sub']);

$generator->setSubject($claims['sub']);
unset($claims['sub']);
}

if (!empty($claims)) {
foreach ($claims as $claim => $value) {
$builder->withClaim($claim, $value);
$generator->addClaim($claim, $value);
}
}

return $builder->getToken($config->signer(), $config->signingKey());
$jwt = $generator->generate();
$parser = new Token\Parser(new JoseEncoder());

// Backwards compatible for signature. In 5.0 this will return a string value
return $parser->parse($jwt);
}
}
101 changes: 91 additions & 10 deletions test/Client/Credentials/KeypairTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

namespace VonageTest\Client\Credentials;

use Lcobucci\JWT\Signer\Key\InMemory;
use Vonage\Client\Exception\Validation;
use VonageTest\VonageTestCase;
use Vonage\Client\Credentials\Keypair;

Expand All @@ -19,8 +21,8 @@

class KeypairTest extends VonageTestCase
{
protected $key;
protected $application = 'c90ddd99-9a5d-455f-8ade-dde4859e590e';
protected string $key;
protected string $application = 'c90ddd99-9a5d-455f-8ade-dde4859e590e';

public function setUp(): void
{
Expand All @@ -36,6 +38,14 @@ public function testAsArray(): void
$this->assertEquals($this->application, $array['application']);
}

public function testGetKey(): void
{
$credentials = new Keypair($this->key, $this->application);

$key = $credentials->getKey();
$this->assertInstanceOf(InMemory::class, $key);
}

public function testProperties(): void
{
$credentials = new Keypair($this->key, $this->application);
Expand All @@ -48,7 +58,6 @@ public function testDefaultJWT(): void
{
$credentials = new Keypair($this->key, $this->application);

//could use the JWT object, but hope to remove as a dependency
$jwt = (string)$credentials->generateJwt()->toString();

[$header, $payload] = $this->decodeJWT($jwt);
Expand All @@ -71,27 +80,100 @@ public function testAdditionalClaims(): void
'nested' => [
'data' => "something"
]
],
'nbf' => 900
]
];

$jwt = $credentials->generateJwt($claims);
[, $payload] = $this->decodeJWT($jwt->toString());

$this->assertArrayHasKey('arbitrary', $payload);
$this->assertEquals($claims['arbitrary'], $payload['arbitrary']);
}

public function testJtiClaim(): void
{
$credentials = new Keypair($this->key, $this->application);

$claims = [
'jti' => '9a1b8ca6-4406-4530-9940-3cde9d41de3f'
];

$jwt = $credentials->generateJwt($claims);
[, $payload] = $this->decodeJWT($jwt->toString());

$this->assertArrayHasKey('jti', $payload);
$this->assertEquals($claims['jti'], $payload['jti']);
}

public function testTtlClaim(): void
{
$credentials = new Keypair($this->key, $this->application);

$claims = [
'ttl' => 900
];

$jwt = $credentials->generateJwt($claims);
[, $payload] = $this->decodeJWT($jwt->toString());

$this->assertArrayHasKey('exp', $payload);
$this->assertEquals(time() + 900, $payload['exp']);
}

public function testNbfNotSupported(): void
{
set_error_handler(static function (int $errno, string $errstr) {
throw new \Exception($errstr, $errno);
}, E_USER_WARNING);

$this->expectExceptionMessage('NotBefore Claim is not supported in Vonage JWT');

$credentials = new Keypair($this->key, $this->application);

$claims = [
'nbf' => time() + 900
];

$jwt = $credentials->generateJwt($claims);
[, $payload] = $this->decodeJWT($jwt->toString());

$this->assertArrayHasKey('arbitrary', $payload);
$this->assertEquals($claims['arbitrary'], $payload['arbitrary']);
$this->assertArrayHasKey('nbf', $payload);
$this->assertEquals(900, $payload['nbf']);

restore_error_handler();
}

public function testExpNotSupported(): void
{
set_error_handler(static function (int $errno, string $errstr) {
throw new \Exception($errstr, $errno);
}, E_USER_WARNING);

$this->expectExceptionMessage('Expiry date is automatically generated from now and TTL, so cannot be passed in
as an argument in claims');

$credentials = new Keypair($this->key, $this->application);

$claims = [
'exp' => time() + 900
];

$jwt = $credentials->generateJwt($claims);
[, $payload] = $this->decodeJWT($jwt->toString());

$this->assertArrayHasKey('arbitrary', $payload);
$this->assertEquals($claims['arbitrary'], $payload['arbitrary']);

restore_error_handler();
}

/**
* @link https://github.com/Vonage/vonage-php-sdk-core/issues/276
*/
public function testExampleConversationJWTWorks()
public function testExampleConversationJWTWorks(): void
{
$credentials = new Keypair($this->key, $this->application);
$claims = [
'exp' => strtotime(date('Y-m-d', strtotime('+24 Hours'))),
'sub' => 'apg-cs',
'acl' => [
'paths' => [
Expand All @@ -113,7 +195,6 @@ public function testExampleConversationJWTWorks()
[, $payload] = $this->decodeJWT($jwt->toString());

$this->assertArrayHasKey('exp', $payload);
$this->assertEquals($claims['exp'], $payload['exp']);
$this->assertEquals($claims['sub'], $payload['sub']);
}

Expand Down
Loading