Skip to content

Commit

Permalink
Merge pull request #13 from clearbooks/use-firebase-jwt
Browse files Browse the repository at this point in the history
Switch to firebase-jwt for PHP 8.1 compatibility
  • Loading branch information
patrick-clearbooks authored Jul 8, 2022
2 parents bdbdd30 + ce80212 commit e8d2370
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 77 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Tests

on: [push, pull_request]

jobs:
run:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.4', '8.0', '8.1']
steps:
- name: Checkout
uses: actions/checkout@v2

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-versions }}
coverage: xdebug

- name: Install dependencies
run: composer install -o --no-progress

- name: Run tests
run: ./vendor/bin/phpunit test
8 changes: 0 additions & 8 deletions .travis.yml

This file was deleted.

4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"type": "library",
"require": {
"php": ">=7.2",
"ext-json": "*",
"clearbooks/dilex": "^3.0.0-alpha.1",
"emarref/jwt": "^1.0.3",
"ext-json": "*"
"firebase/php-jwt": "^6.2"

},
"require-dev": {
Expand Down
58 changes: 28 additions & 30 deletions src/JwtGuard/JwtTokenAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,9 @@
namespace Clearbooks\Dilex\JwtGuard;

use DateTime;
use Emarref\Jwt\Algorithm\AlgorithmInterface;
use Emarref\Jwt\Algorithm\None;
use Emarref\Jwt\Encryption\Factory as EncryptionFactory;
use Emarref\Jwt\Exception\VerificationException;
use Emarref\Jwt\Jwt;
use Emarref\Jwt\Token;
use Emarref\Jwt\Verification\Context;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use stdClass;
use Symfony\Component\HttpFoundation\Request;

class JwtTokenAuthenticator implements RequestAuthoriser, IdentityProvider
Expand All @@ -22,12 +18,12 @@ class JwtTokenAuthenticator implements RequestAuthoriser, IdentityProvider
const BEARER = 'Bearer ';

/**
* @var AlgorithmInterface
* @var Key
*/
protected $algorithm;
protected $key;

/**
* @var Token
* @var stdClass
*/
protected $token;

Expand All @@ -37,15 +33,13 @@ class JwtTokenAuthenticator implements RequestAuthoriser, IdentityProvider
protected $appIdProvider;

/**
* @param Jwt $jwt
* @param AlgorithmInterface $algorithm
* @param Key $key
* @param AppIdProvider $appIdProvider
*/
public function __construct( Jwt $jwt, AlgorithmInterface $algorithm, AppIdProvider $appIdProvider )
public function __construct( Key $key, AppIdProvider $appIdProvider )
{
$this->jwt = $jwt;
$this->algorithm = $algorithm;
$this->token = new Token;
$this->key = $key;
$this->token = new stdClass;
$this->appIdProvider = $appIdProvider;
}

Expand All @@ -56,8 +50,7 @@ public function __construct( Jwt $jwt, AlgorithmInterface $algorithm, AppIdProvi
*/
protected function getClaimOrNull( $claim )
{
$claim = $this->token->getPayload()->findClaimByName( $claim );
return $claim ? $claim->getValue() : null;
return $this->token->$claim ?? null;
}

/**
Expand Down Expand Up @@ -96,29 +89,24 @@ protected function isAllowedAppId()
public function isAuthorised( Request $request )
{
$header = $request->headers->get( 'Authorization' );
$context = new Context( EncryptionFactory::create( $this->algorithm ) );

if (strtolower($this->key->getAlgorithm()) === 'none') {
return false;
}

if ( $header ) {
try{
$this->token = $this->jwt->deserialize( $this->extractJwtFromHeader($header) );
$this->token = JWT::decode( $this->extractJwtFromHeader($header), $this->key );
} catch ( \Exception $e ){
return false;
}
}

if ( $this->algorithm instanceof None ) {
return false;
}

if( $this->isExpired() || !$this->hasUserId() || !$this->isAllowedAppId() ) {
return false;
}

try {
return $this->jwt->verify( $this->token, $context );
} catch ( VerificationException $e ) {
return false;
}
return true;
}

/**
Expand Down Expand Up @@ -153,7 +141,17 @@ public function getIsAdmin()
public function getSegments()
{
$segments = $this->getClaimOrNull( self::SEGMENTS );
return is_array( $segments ) ? $segments : [ ];

if (!is_array($segments)) {
return [];
}

return array_map(
static function (stdClass $segment): array {
return (array) $segment;
},
$segments
);
}

protected function extractJwtFromHeader( $header )
Expand Down
73 changes: 36 additions & 37 deletions test/JwtGuard/JwtTokenAuthenticatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,10 @@
namespace Clearbooks\Dilex\JwtGuard;

use DateTime;
use Emarref\Jwt\Algorithm\Hs512;
use Emarref\Jwt\Algorithm\None;
use Emarref\Jwt\Claim\PublicClaim;
use Emarref\Jwt\Encryption\Factory as EncryptionFactory;
use Emarref\Jwt\Jwt;
use Emarref\Jwt\Token;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use PHPUnit\Framework\TestCase;
use stdClass;
use Symfony\Component\HttpFoundation\Request;

class JwtTokenAuthenticatorTest extends TestCase
Expand Down Expand Up @@ -43,17 +40,17 @@ class JwtTokenAuthenticatorTest extends TestCase


/**
* @var Hs512
* @var Key
*/
private $algorithm;
private $key;

/**
* @var JwtTokenAuthenticator
*/
private $auth;

/**
* @var Token
* @var stdClass
*/
private $token;

Expand Down Expand Up @@ -89,93 +86,94 @@ private function getExpiredDate()

/**
* @param array $spec
* @return Token
* @return stdClass
*/
private function getTokenWithout( array $spec )
{
$mappings = [
self::VALID_USER_ID => new PublicClaim( 'userId', self::USER_ID ),
self::VALID_GROUP_ID => new PublicClaim( 'groupId', self::GROUP_ID ),
self::VALID_APP_ID => new PublicClaim( 'appId', self::APP_ID ),
self::VALID_EXPIRY_DATE => new PublicClaim('exp', $this->getNonExpiredDate()),
self::VALID_IS_ADMIN => new PublicClaim('isAdmin', self::IS_ADMIN),
self::VALID_SEGMENTS => new PublicClaim('segments', $this->testSegments)
self::VALID_USER_ID => [ 'userId', self::USER_ID ],
self::VALID_GROUP_ID => [ 'groupId', self::GROUP_ID ],
self::VALID_APP_ID => [ 'appId', self::APP_ID ],
self::VALID_EXPIRY_DATE => ['exp', $this->getNonExpiredDate()],
self::VALID_IS_ADMIN => ['isAdmin', self::IS_ADMIN],
self::VALID_SEGMENTS => ['segments', $this->testSegments]
];

$spec = array_diff( array_keys( $mappings ), $spec );

$token = new Token;
$token = new stdClass();
foreach ( $spec as $desiredClaim ) {
$token->addClaim( $mappings[$desiredClaim] );
$claim = $mappings[$desiredClaim];
$token->{$claim[0]} = $claim[1];
}
return $token;
}

/**
* @return Token
* @return stdClass
*/
private function getTokenWithNoAppId()
{
return $this->getTokenWithout( [self::VALID_APP_ID] );
}

/**
* @return Token
* @return stdClass
*/
private function getTokenWithNoUserId()
{
return $this->getTokenWithout( [self::VALID_USER_ID] );
}

/**
* @return Token
* @return stdClass
*/
private function getTokenWithNoGroupId()
{
return $this->getTokenWithout( [self::VALID_GROUP_ID] );
}

/**
* @return Token
* @return stdClass
*/
private function getTokenWithoutSegments()
{
return $this->getTokenWithout( [self::VALID_SEGMENTS] );
}

/**
* @return Token
* @return stdClass
*/
private function getTokenWithInvalidAppId()
{
$token = $this->getTokenWithout( [self::VALID_APP_ID] );
$token->addClaim( new PublicClaim( 'appId', 'dogs' ) );
$token->appId = 'dogs';
return $token;
}

/**
* @return Token
* @return stdClass
*/
private function getValidToken()
{
return $this->getTokenWithout( [] );
}

/**
* @return Token
* @return stdClass
*/
private function getExpiredToken()
{
$token = $this->getTokenWithout( [self::VALID_EXPIRY_DATE] );
$token->addClaim(new PublicClaim('exp', $this->getExpiredDate()));
$token->exp = $this->getExpiredDate();
return $token;
}

/**
* @param Token $token
* @param stdClass $token
* @return bool
*/
private function authoriseToken( Token $token )
private function authoriseToken( stdClass $token )
{
return $this->auth->isAuthorised( new MockTokenRequest( $this->serialiseToken( $token ) ) );
}
Expand All @@ -186,7 +184,7 @@ private function authoriseToken( Token $token )
*/
private function serialiseToken( $token )
{
return ( new Jwt )->serialize( $token, EncryptionFactory::create( $this->algorithm ) );
return JWT::encode((array) $token, $this->key->getKeyMaterial(), $this->key->getAlgorithm());
}

/**
Expand All @@ -195,19 +193,20 @@ private function serialiseToken( $token )
public function setUp(): void
{
$this->appIds = new StaticAppIdProvider( [self::APP_ID] );
$this->algorithm = new Hs512( "shhh... it's a secret" );
$this->auth = new JwtTokenAuthenticator( new Jwt, $this->algorithm, $this->appIds );
$this->token = new Token();
$this->key = new Key( "shhh... it's a secret", 'HS512' );
$this->auth = new JwtTokenAuthenticator( $this->key, $this->appIds );
$this->token = new stdClass();
$this->testSegments = [ [ 'segmentId' => 1, 'isLocked' => false, 'priority' => 10 ] ];
}

/**
* @test
*/
public function givenNoneAlgorithm_returnFalse()
public function givenNoneAlgorithm_throwsException()
{
$auth = new JwtTokenAuthenticator( $jwt = new Jwt, new None, $this->appIds );
$this->assertFalse( $auth->isAuthorised( new MockTokenRequest( $jwt->serialize( new Token, EncryptionFactory::create( new None ) ) ) ) );
self::expectException(\DomainException::class);
$auth = new JwtTokenAuthenticator( new Key(' ', 'None'), $this->appIds );
$auth->isAuthorised( new MockTokenRequest( JWT::encode([], ' ', 'None')) );
}

/**
Expand Down Expand Up @@ -298,7 +297,7 @@ public function givenTokenWithNoAppId_whenVerifyingToken_returnFalse()
*/
public function givenTokenWithInvalidSignature_whenValidatingToken_returnFalse()
{
$this->auth = new JwtTokenAuthenticator( new Jwt, new Hs512( 'Nope' ), $this->appIds );
$this->auth = new JwtTokenAuthenticator( new Key( 'Nope', 'HS512' ), $this->appIds );
$this->assertFalse( $this->authoriseToken( $this->getValidToken() ) );
}

Expand Down

0 comments on commit e8d2370

Please sign in to comment.