Skip to content

Commit

Permalink
bug #797 Fix support for lcobucci/jwt v3.4 and 4.0 (chalasr)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the 2.x-dev branch.

Discussion
----------

Fix support for lcobucci/jwt v3.4 and 4.0

Fixes #795
Fixes #793

Commits
-------

56902ac Fix compat with lcobucci/jwt v4
252009a Fix compat with lcobucci/jwt v3.4
  • Loading branch information
chalasr committed Nov 28, 2020
2 parents 3c9f4e7 + 56902ac commit 4d22398
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 28 deletions.
143 changes: 118 additions & 25 deletions Services/JWSProvider/LcobucciJWSProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@

namespace Lexik\Bundle\JWTAuthenticationBundle\Services\JWSProvider;

use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Encoding\MicrosecondBasedDateConversion;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Token\Parser as JWTParser;
use Lcobucci\JWT\Signer;
use Lcobucci\JWT\Signer\Hmac;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\Token\Builder as JWTBuilder;
use Lcobucci\JWT\Token\Plain;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\Constraint\ValidAt;
use Lcobucci\JWT\Validation\Validator;
use Lcobucci\JWT\ValidationData;
use Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\KeyLoaderInterface;
use Lexik\Bundle\JWTAuthenticationBundle\Services\KeyLoader\RawKeyLoader;
Expand Down Expand Up @@ -41,6 +51,11 @@ class LcobucciJWSProvider implements JWSProviderInterface
*/
private $clockSkew;

/**
* @var bool
*/
private $legacyJWTApi;

/**
* @param KeyLoaderInterface $keyLoader
* @param string $cryptoEngine
Expand Down Expand Up @@ -68,50 +83,101 @@ public function __construct(KeyLoaderInterface $keyLoader, $cryptoEngine, $signa
$this->signer = $this->getSignerForAlgorithm($signatureAlgorithm);
$this->ttl = $ttl;
$this->clockSkew = $clockSkew;
$this->legacyJWTApi = !method_exists(Builder::class, 'withHeader');
}

/**
* {@inheritdoc}
*/
public function create(array $payload, array $header = [])
{
$jws = new Builder();
if (class_exists(JWTBuilder::class)) {
$jws = new JWTBuilder(new JoseEncoder(), new MicrosecondBasedDateConversion());
} else {
$jws = new Builder();
}

foreach ($header as $k => $v) {
$jws->setHeader($k, $v);
$jws->{$this->legacyJWTApi ? 'setHeader' : 'withHeader'}($k, $v);
}
$jws->setIssuedAt(time());

if (null !== $this->ttl && !isset($payload['exp'])) {
$jws->setExpiration(time() + $this->ttl);
$now = time();

if ($this->legacyJWTApi) {
$jws->setIssuedAt($now);
} else {
$jws->issuedAt(new \DateTimeImmutable("@{$now}"));
}

if (null !== $this->ttl || isset($payload['exp'])) {
$exp = isset($payload['exp']) ? $payload['exp'] : $now + $this->ttl;
unset($payload['exp']);

if ($this->legacyJWTApi) {
$jws->setExpiration($exp);
} else {
$jws->expiresAt($exp instanceof \DateTimeImmutable ? $exp : new \DateTimeImmutable("@$exp"));
}
}

if (isset($payload['sub'])) {
$jws->{$this->legacyJWTApi ? 'setSubject' : 'relatedTo'}($payload['sub']);
unset($payload['sub']);
}

foreach ($payload as $name => $value) {
$jws->set($name, $value);
if ($this->legacyJWTApi) {
$jws->set($name, $value);
} else {
$jws->{method_exists($jws,'with') ? 'with' : 'withClaim'}($name, $value);
}
}

$e = null;
$e = $token = null;

try {
$this->sign($jws);
$token = $this->getSignedToken($jws);
} catch (\InvalidArgumentException $e) {
}

return new CreatedJWS((string) $jws->getToken(), null === $e);
return new CreatedJWS((string) $token, null === $e);
}

/**
* {@inheritdoc}
*/
public function load($token)
{
$jws = (new Parser())->parse((string) $token);
if (class_exists(JWTParser::class)) {
$jws = (new JWTParser(new JoseEncoder()))->parse((string) $token);
} else {
$jws = (new Parser())->parse((string) $token);
}

$payload = [];
foreach ($jws->getClaims() as $claim) {
$payload[$claim->getName()] = $claim->getValue();

if ($this->legacyJWTApi) {
foreach ($jws->getClaims() as $claim) {
$payload[$claim->getName()] = $claim->getValue();
}
} else {
foreach ($jws->claims()->all() as $name => $value) {
if ($value instanceof \DateTimeInterface) {
$value = $value->getTimestamp();
}
$payload[$name] = $value;
}
}

return new LoadedJWS($payload, $this->verify($jws), null !== $this->ttl, $jws->getHeaders(), $this->clockSkew);
$jws = new LoadedJWS(
$payload,
$this->verify($jws),
null !== $this->ttl,
$this->legacyJWTApi ? $jws->getHeaders() : $jws->headers()->all(),
$this->clockSkew
);

return $jws;
}

private function getSignerForAlgorithm($signatureAlgorithm)
Expand Down Expand Up @@ -139,28 +205,55 @@ private function getSignerForAlgorithm($signatureAlgorithm)
return new $signerClass();
}

private function sign(Builder $jws)
private function getSignedToken(Builder $jws)
{
if ($this->signer instanceof Hmac) {
return $jws->sign($this->signer, $this->keyLoader->loadKey(RawKeyLoader::TYPE_PRIVATE));
if (class_exists(InMemory::class)) {
$key = InMemory::plainText($this->keyLoader->loadKey(RawKeyLoader::TYPE_PRIVATE), $this->signer instanceof Hmac ? '' : $this->keyLoader->getPassphrase());
} else {
new Key($this->keyLoader->loadKey(RawKeyLoader::TYPE_PRIVATE), $this->signer instanceof Hmac ? '' : $this->keyLoader->getPassphrase());
}

return $jws->sign(
$this->signer,
new Key($this->keyLoader->loadKey(RawKeyLoader::TYPE_PRIVATE), $this->keyLoader->getPassphrase())
);
if ($this->legacyJWTApi) {
$jws->sign($key, $this->signer);

return $jws->getToken();
}

$token = $jws->getToken($this->signer, $key);

if (!$token instanceof Plain) {
return (string) $token;
}

return $token->toString();
}

private function verify(Token $jwt)
{
if (!$jwt->validate(new ValidationData(time() + $this->clockSkew))) {
return false;
if ($this->legacyJWTApi) {
if (!$jwt->validate(new ValidationData(time() + $this->clockSkew))) {
return false;
}

return $jwt->verify(
$this->signer,
$this->signer instanceof Hmac ? $this->keyLoader->loadKey(RawKeyLoader::TYPE_PRIVATE) : $this->keyLoader->loadKey(RawKeyLoader::TYPE_PUBLIC)
);
}

if ($this->signer instanceof Hmac) {
return $jwt->verify($this->signer, $this->keyLoader->loadKey(RawKeyLoader::TYPE_PRIVATE));
if (class_exists(InMemory::class)) {
$key = InMemory::plainText($this->signer instanceof Hmac ? $this->keyLoader->loadKey(RawKeyLoader::TYPE_PRIVATE) : $this->keyLoader->loadKey(RawKeyLoader::TYPE_PUBLIC));
} else {
$key = new Key($this->signer instanceof Hmac ? $this->keyLoader->loadKey(RawKeyLoader::TYPE_PRIVATE) : $this->keyLoader->loadKey(RawKeyLoader::TYPE_PUBLIC));
}

return $jwt->verify($this->signer, $this->keyLoader->loadKey(RawKeyLoader::TYPE_PUBLIC));
$clock = SystemClock::fromUTC();
$validator = new Validator();

return $validator->validate(
$jwt,
new ValidAt($clock, new \DateInterval("PT{$this->clockSkew}S")),
new SignedWith($this->signer, $key)
);
}
}
12 changes: 10 additions & 2 deletions Tests/Functional/GetTokenTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace Lexik\Bundle\JWTAuthenticationBundle\Tests\Functional;

use Lcobucci\JWT\Encoding\JoseEncoder;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Token\Parser as JWTParser;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTDecodedEvent;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
Expand Down Expand Up @@ -68,8 +70,14 @@ public function testGetTokenWithCustomClaim()
$this->assertArrayHasKey('custom', $payload, 'The payload should contains a "custom" claim.');
$this->assertSame('dummy', $payload['custom'], 'The "custom" claim should be equal to "dummy".');

$jws = (new Parser())->parse($token);
$this->assertArrayHasKey('foo', $jws->getHeaders(), 'The payload should contains a custom "foo" header.');
if (class_exists(JWTParser::class)) {
$parser = new JWTParser(new JoseEncoder());
} else {
$parser = new Parser();
}
$jws = $parser->parse($token);

$this->assertArrayHasKey('foo', method_exists($jws, 'headers') ? $jws->headers()->all() : $jws->getHeaders(), 'The payload should contains a custom "foo" header.');
}

public function testGetTokenFromInvalidCredentials()
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"require": {
"php": ">=5.6",
"ext-openssl": "*",
"lcobucci/jwt": "^3.2",
"lcobucci/jwt": "^3.2|^4.0",
"namshi/jose": "^7.2",
"symfony/framework-bundle": "^3.4|^4.0|^5.0",
"symfony/security-bundle": "^3.4|^4.0|^5.0"
Expand Down

0 comments on commit 4d22398

Please sign in to comment.