diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..d6c3bc5 --- /dev/null +++ b/.github/workflows/tests.yml @@ -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 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d64d496..0000000 --- a/.travis.yml +++ /dev/null @@ -1,8 +0,0 @@ -language: php -php: - - 7.2 - - 7.3 - - 7.4 - -install: composer install -script: vendor/phpunit/phpunit/phpunit --coverage-clover build/logs/clover.xml test diff --git a/composer.json b/composer.json index 978511d..98f7aea 100644 --- a/composer.json +++ b/composer.json @@ -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": { diff --git a/src/JwtGuard/JwtTokenAuthenticator.php b/src/JwtGuard/JwtTokenAuthenticator.php index 778bd53..44c3f67 100644 --- a/src/JwtGuard/JwtTokenAuthenticator.php +++ b/src/JwtGuard/JwtTokenAuthenticator.php @@ -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 @@ -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; @@ -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; } @@ -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; } /** @@ -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; } /** @@ -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 ) diff --git a/test/JwtGuard/JwtTokenAuthenticatorTest.php b/test/JwtGuard/JwtTokenAuthenticatorTest.php index 5fc3d71..503a87c 100644 --- a/test/JwtGuard/JwtTokenAuthenticatorTest.php +++ b/test/JwtGuard/JwtTokenAuthenticatorTest.php @@ -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 @@ -43,9 +40,9 @@ class JwtTokenAuthenticatorTest extends TestCase /** - * @var Hs512 + * @var Key */ - private $algorithm; + private $key; /** * @var JwtTokenAuthenticator @@ -53,7 +50,7 @@ class JwtTokenAuthenticatorTest extends TestCase private $auth; /** - * @var Token + * @var stdClass */ private $token; @@ -89,30 +86,31 @@ 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() { @@ -120,7 +118,7 @@ private function getTokenWithNoAppId() } /** - * @return Token + * @return stdClass */ private function getTokenWithNoUserId() { @@ -128,7 +126,7 @@ private function getTokenWithNoUserId() } /** - * @return Token + * @return stdClass */ private function getTokenWithNoGroupId() { @@ -136,7 +134,7 @@ private function getTokenWithNoGroupId() } /** - * @return Token + * @return stdClass */ private function getTokenWithoutSegments() { @@ -144,17 +142,17 @@ private function getTokenWithoutSegments() } /** - * @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() { @@ -162,20 +160,20 @@ private function getValidToken() } /** - * @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 ) ) ); } @@ -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()); } /** @@ -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')) ); } /** @@ -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() ) ); }