diff --git a/src/Parser.php b/src/Parser.php new file mode 100644 index 00000000..ebee35dd --- /dev/null +++ b/src/Parser.php @@ -0,0 +1,164 @@ + + * @since 0.1.0 + */ +class Parser +{ + /** + * The data encoder + * + * @var Encoder + */ + private $encoder; + + /** + * The data decoder + * + * @var Decoder + */ + private $decoder; + + /** + * The signer factory + * + * @var Factory + */ + private $factory; + + /** + * Initializes the object + * + * @param Encoder $encoder + * @param Decoder $decoder + * @param Factory $factory + */ + public function __construct( + Encoder $encoder = null, + Decoder $decoder = null, + Factory $factory = null + ) { + $this->encoder = $encoder ?: new Encoder(); + $this->decoder = $decoder ?: new Decoder(); + $this->factory = $factory ?: new Factory(); + } + + /** + * Parses the JWT and returns a token + * + * @param string $jwt + * @return Token + */ + public function parse($jwt) + { + $data = $this->splitJwt($jwt); + + $token = new Token( + $header = $this->parseHeader($data[0]), + $this->parseClaims($data[1]), + $this->parseSignature($header, $data[2]) + ); + + $token->setEncoder($this->encoder); + + return $token; + } + + /** + * Slipts the JWT string into an array + * + * @param string $jwt + * + * @return array + * + * @throws InvalidArgumentException When JWT is not a string or is invalid + */ + protected function splitJwt($jwt) + { + if (!is_string($jwt)) { + throw new InvalidArgumentException('The JWT string must have two dots'); + } + + $data = explode('.', $jwt); + + if (count($data) != 3) { + throw new InvalidArgumentException('The JWT string must have two dots'); + } + + return $data; + } + + /** + * Parses the header from a string + * + * @param string $data + * + * @return array + * + * @throws InvalidArgumentException When an invalid header is informed + */ + protected function parseHeader($data) + { + $header = $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data)); + + if (!is_array($header) || isset($header['enc'])) { + throw new InvalidArgumentException('That header is not a valid array or uses encryption'); + } + + return $header; + } + + /** + * Parses the claim set from a string + * + * @param string $data + * + * @return array + * + * @throws InvalidArgumentException When an invalid claim set is informed + */ + protected function parseClaims($data) + { + $claims = $this->decoder->jsonDecode($this->decoder->base64UrlDecode($data)); + + if (!is_array($claims)) { + throw new InvalidArgumentException('That claims are not valid'); + } + + return $claims; + } + + /** + * Returns the signature from given data + * + * @param array $header + * @param string $data + * + * @return Signature + */ + protected function parseSignature(array $header, $data) + { + if ($data == '' || !isset($header['alg']) || $header['alg'] == 'none') { + return null; + } + + $hash = $this->decoder->base64UrlDecode($data); + + return new Signature($this->factory->create($header['alg']), $hash); + } +} diff --git a/test/ParserTest.php b/test/ParserTest.php new file mode 100644 index 00000000..f28f3705 --- /dev/null +++ b/test/ParserTest.php @@ -0,0 +1,261 @@ + + * @since 0.1.0 + * + * @coversDefaultClass Lcobucci\JWT\Parser + */ +class ParserTest extends \PHPUnit_Framework_TestCase +{ + /** + * @var Encoder|\PHPUnit_Framework_MockObject_MockObject + */ + protected $encoder; + + /** + * @var Decoder|\PHPUnit_Framework_MockObject_MockObject + */ + protected $decoder; + + /** + * @var Factory|\PHPUnit_Framework_MockObject_MockObject + */ + protected $factory; + + /** + * {@inheritdoc} + */ + protected function setUp() + { + $this->encoder = $this->getMockBuilder(Encoder::class) + ->setMockClassName('EncoderMock') + ->getMock(); + + $this->decoder = $this->getMockBuilder(Decoder::class) + ->setMockClassName('DecoderMock') + ->getMock(); + + $this->factory = $this->getMockBuilder(Factory::class) + ->setMockClassName('FactoryMock') + ->getMock(); + } + + /** + * @test + * @covers ::__construct + */ + public function constructMustConfigureTheAttributes() + { + $parser = new Parser($this->encoder, $this->decoder, $this->factory); + + $this->assertAttributeSame($this->encoder, 'encoder', $parser); + $this->assertAttributeSame($this->decoder, 'decoder', $parser); + $this->assertAttributeSame($this->factory, 'factory', $parser); + } + + /** + * @test + * @covers ::__construct + * @covers ::parse + * @covers ::splitJwt + * + * @expectedException InvalidArgumentException + */ + public function parseMustRaiseExceptionWhenJWSIsNotAString() + { + $parser = new Parser($this->encoder, $this->decoder, $this->factory); + + $parser->parse(['asdasd']); + } + + /** + * @test + * @covers ::__construct + * @covers ::parse + * @covers ::splitJwt + * + * @expectedException InvalidArgumentException + */ + public function parseMustRaiseExceptionWhenJWSDontHaveThreeParts() + { + $parser = new Parser($this->encoder, $this->decoder, $this->factory); + + $parser->parse(''); + } + + /** + * @test + * @covers ::__construct + * @covers ::parse + * @covers ::splitJwt + * @covers ::parseHeader + * + * @expectedException RuntimeException + */ + public function parseMustRaiseExceptionWhenHeaderCannotBeDecoded() + { + $this->decoder->expects($this->any()) + ->method('jsonDecode') + ->willThrowException(new RuntimeException()); + + $parser = new Parser($this->encoder, $this->decoder, $this->factory); + + $parser->parse('asdfad.asdfasdf.'); + } + + /** + * @test + * @covers ::__construct + * @covers ::parse + * @covers ::splitJwt + * @covers ::parseHeader + * + * @expectedException InvalidArgumentException + */ + public function parseMustRaiseExceptionWhenHeaderIsNotAnArray() + { + $this->decoder->expects($this->any()) + ->method('jsonDecode') + ->willReturn('asdfasdfasd'); + + $parser = new Parser($this->encoder, $this->decoder, $this->factory); + + $parser->parse('a.a.'); + } + + /** + * @test + * @covers ::__construct + * @covers ::parse + * @covers ::splitJwt + * @covers ::parseHeader + * + * @expectedException InvalidArgumentException + */ + public function parseMustRaiseExceptionWhenHeaderIsFromAnEncryptedToken() + { + $this->decoder->expects($this->any()) + ->method('jsonDecode') + ->willReturn(['enc' => 'AAA']); + + $parser = new Parser($this->encoder, $this->decoder, $this->factory); + + $parser->parse('a.a.'); + } + + /** + * @test + * @covers ::__construct + * @covers ::parse + * @covers ::splitJwt + * @covers ::parseHeader + * @covers ::parseClaims + * + * @expectedException InvalidArgumentException + */ + public function parseMustRaiseExceptionWhenClaimSetIsNotAnArray() + { + $this->decoder->expects($this->at(1)) + ->method('jsonDecode') + ->willReturn(['typ' => 'JWT', 'alg' => 'none']); + + $this->decoder->expects($this->at(3)) + ->method('jsonDecode') + ->willReturn('asdfasdfasd'); + + $parser = new Parser($this->encoder, $this->decoder, $this->factory); + + $parser->parse('a.a.'); + } + + /** + * @test + * @covers ::__construct + * @covers ::parse + * @covers ::splitJwt + * @covers ::parseHeader + * @covers ::parseClaims + * @covers ::parseSignature + * @covers Lcobucci\JWT\Token::__construct + * @covers Lcobucci\JWT\Token::setEncoder + */ + public function parseMustReturnANonSignedTokenWhenSignatureIsNotInformed() + { + $this->decoder->expects($this->at(1)) + ->method('jsonDecode') + ->willReturn(['typ' => 'JWT', 'alg' => 'none']); + + $this->decoder->expects($this->at(3)) + ->method('jsonDecode') + ->willReturn(['aud' => 'test']); + + $parser = new Parser($this->encoder, $this->decoder, $this->factory); + + $token = $parser->parse('a.a.'); + + $this->assertAttributeEquals(['typ' => 'JWT', 'alg' => 'none'], 'header', $token); + $this->assertAttributeEquals(['aud' => 'test'], 'claims', $token); + $this->assertAttributeEquals(null, 'signature', $token); + $this->assertAttributeSame($this->encoder, 'encoder', $token); + } + + /** + * @test + * @covers ::__construct + * @covers ::parse + * @covers ::splitJwt + * @covers ::parseHeader + * @covers ::parseClaims + * @covers ::parseSignature + * @covers Lcobucci\JWT\Token::__construct + * @covers Lcobucci\JWT\Token::setEncoder + * @covers Lcobucci\JWT\Signature::__construct + */ + public function parseMustReturnASignedTokenWhenSignatureIsInformed() + { + $signer = $this->getMockBuilder(Signer::class) + ->setMockClassName('SignerMock') + ->getMock(); + + $this->decoder->expects($this->at(1)) + ->method('jsonDecode') + ->willReturn(['typ' => 'JWT', 'alg' => 'HS256']); + + $this->decoder->expects($this->at(3)) + ->method('jsonDecode') + ->willReturn(['aud' => 'test']); + + $this->decoder->expects($this->at(4)) + ->method('base64UrlDecode') + ->willReturn('aaa'); + + $this->factory->expects($this->any()) + ->method('create') + ->willReturn($signer); + + $parser = new Parser($this->encoder, $this->decoder, $this->factory); + + $token = $parser->parse('a.a.a'); + + $this->assertAttributeEquals(['typ' => 'JWT', 'alg' => 'HS256'], 'header', $token); + $this->assertAttributeEquals(['aud' => 'test'], 'claims', $token); + $this->assertAttributeEquals(new Signature($signer, 'aaa'), 'signature', $token); + $this->assertAttributeSame($this->encoder, 'encoder', $token); + } +}