summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNeuman Vong <neuman@twilio.com>2011-03-25 16:08:00 -0700
committerNeuman Vong <neuman@twilio.com>2011-03-25 16:10:34 -0700
commit24e03229ce504897bf7674039367905469d19ef7 (patch)
tree6235af5773ff43296bad4c5ad3c814d99026a832
downloadphp-jwt-24e03229ce504897bf7674039367905469d19ef7.zip
php-jwt-24e03229ce504897bf7674039367905469d19ef7.tar.gz
php-jwt-24e03229ce504897bf7674039367905469d19ef7.tar.bz2
Start tracking project
-rw-r--r--JWT.php164
-rw-r--r--Makefile6
-rw-r--r--package.php75
-rw-r--r--package.xml75
-rw-r--r--tests/Bootstrap.php9
-rw-r--r--tests/JWTTest.php31
-rw-r--r--tests/phpunit.xml7
7 files changed, 367 insertions, 0 deletions
diff --git a/JWT.php b/JWT.php
new file mode 100644
index 0000000..d1704e1
--- /dev/null
+++ b/JWT.php
@@ -0,0 +1,164 @@
+<?php
+
+/**
+ * JSON Web Token implementation
+ *
+ * Minimum implementation used by Realtime auth, based on this spec:
+ * http://self-issued.info/docs/draft-jones-json-web-token-01.html.
+ *
+ * @author Neuman Vong <neuman@twilio.com>
+ */
+class JWT
+{
+ /**
+ * @param string $jwt The JWT
+ * @param string|null $key The secret key
+ * @param bool $verify Don't skip verification process
+ *
+ * @return object The JWT's payload as a PHP object
+ */
+ public static function decode($jwt, $key = null, $verify = true)
+ {
+ $tks = explode('.', $jwt);
+ if (count($tks) != 3) {
+ throw new UnexpectedValueException('Wrong number of segments');
+ }
+ list($headb64, $payloadb64, $cryptob64) = $tks;
+ if (null === ($header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64)))
+ ) {
+ throw new UnexpectedValueException('Invalid segment encoding');
+ }
+ if (null === $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($payloadb64))
+ ) {
+ throw new UnexpectedValueException('Invalid segment encoding');
+ }
+ $sig = JWT::urlsafeB64Decode($cryptob64);
+ if ($verify) {
+ if (empty($header->alg)) {
+ throw new DomainException('Empty algorithm');
+ }
+ if ($sig != JWT::sign("$headb64.$payloadb64", $key, $header->alg)) {
+ throw new UnexpectedValueException('Signature verification failed');
+ }
+ }
+ return $payload;
+ }
+
+ /**
+ * @param object|array $payload PHP object or array
+ * @param string $key The secret key
+ * @param string $algo The signing algorithm
+ *
+ * @return string A JWT
+ */
+ public static function encode($payload, $key, $algo = 'HS256')
+ {
+ $header = array('typ' => 'jwt', 'alg' => $algo);
+
+ $segments = array();
+ $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($header));
+ $segments[] = JWT::urlsafeB64Encode(JWT::jsonEncode($payload));
+ $signing_input = implode('.', $segments);
+
+ $signature = JWT::sign($signing_input, $key, $algo);
+ $segments[] = JWT::urlsafeB64Encode($signature);
+
+ return implode('.', $segments);
+ }
+
+ /**
+ * @param string $msg The message to sign
+ * @param string $key The secret key
+ * @param string $method The signing algorithm
+ *
+ * @return string An encrypted message
+ */
+ public static function sign($msg, $key, $method = 'HS256')
+ {
+ $methods = array(
+ 'HS256' => 'sha256',
+ 'HS384' => 'sha384',
+ 'HS512' => 'sha512',
+ );
+ if (empty($methods[$method])) {
+ throw new DomainException('Algorithm not supported');
+ }
+ return hash_hmac($methods[$method], $msg, $key, true);
+ }
+
+ /**
+ * @param string $input JSON string
+ *
+ * @return object Object representation of JSON string
+ */
+ public static function jsonDecode($input)
+ {
+ $obj = json_decode($input);
+ if (function_exists('json_last_error') && $errno = json_last_error()) {
+ JWT::handleJsonError($errno);
+ }
+ else if ($obj === null && $input !== 'null') {
+ throw new DomainException('Null result with non-null input');
+ }
+ return $obj;
+ }
+
+ /**
+ * @param object|array $input A PHP object or array
+ *
+ * @return string JSON representation of the PHP object or array
+ */
+ public static function jsonEncode($input)
+ {
+ $json = json_encode($input);
+ if (function_exists('json_last_error') && $errno = json_last_error()) {
+ JWT::handleJsonError($errno);
+ }
+ else if ($json === 'null' && $input !== null) {
+ throw new DomainException('Null result with non-null input');
+ }
+ return $json;
+ }
+
+ /**
+ * @param string $input A base64 encoded string
+ *
+ * @return string A decoded string
+ */
+ public static function urlsafeB64Decode($input)
+ {
+ $padlen = 4 - strlen($input) % 4;
+ $input .= str_repeat('=', $padlen);
+ return base64_decode(strtr($input, '-_', '+/'));
+ }
+
+ /**
+ * @param string $input Anything really
+ *
+ * @return string The base64 encode of what you passed in
+ */
+ public static function urlsafeB64Encode($input)
+ {
+ return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
+ }
+
+ /**
+ * @param int $errno An error number from json_last_error()
+ *
+ * @return void
+ */
+ private static function handleJsonError($errno)
+ {
+ $messages = array(
+ JSON_ERROR_DEPTH => 'Maximum stack depth exceeded',
+ JSON_ERROR_CTRL_CHAR => 'Unexpected control character found',
+ JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON'
+ );
+ throw new DomainException(isset($messages[$errno])
+ ? $messages[$errno]
+ : 'Unknown JSON error: ' . $errno
+ );
+ }
+
+}
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..be761d5
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,6 @@
+all: test
+test:
+ @echo running tests
+ @phpunit --configuration tests/phpunit.xml
+
+.PHONY: all test
diff --git a/package.php b/package.php
new file mode 100644
index 0000000..83d50bd
--- /dev/null
+++ b/package.php
@@ -0,0 +1,75 @@
+<?php
+
+require_once 'PEAR/PackageFileManager2.php';
+PEAR::setErrorHandling(PEAR_ERROR_DIE);
+
+$api_version = '0.0.0';
+$api_state = 'alpha';
+
+$release_version = '0.0.0';
+$release_state = 'alpha';
+$release_notes = "No release notes.";
+
+$description = <<<DESC
+A JWT encoder/decoder.
+DESC;
+
+$package = new PEAR_PackageFileManager2();
+
+$package->setOptions(
+ array(
+ 'filelistgenerator' => 'file',
+ 'simpleoutput' => true,
+ 'baseinstalldir' => '/',
+ 'packagedirectory' => './',
+ 'dir_roles' => array(
+ 'tests' => 'test'
+ ),
+ 'ignore' => array(
+ 'package.php',
+ '*.tgz'
+ )
+ )
+);
+
+$package->setPackage('JWT');
+$package->setSummary('A JWT encoder/decoder.');
+$package->setDescription($description);
+$package->setChannel('pear.php.net');
+$package->setPackageType('php');
+$package->setLicense(
+ 'MIT License',
+ 'http://creativecommons.org/licenses/MIT/'
+);
+
+$package->setNotes($release_notes);
+$package->setReleaseVersion($release_version);
+$package->setReleaseStability($release_state);
+$package->setAPIVersion($api_version);
+$package->setAPIStability($api_state);
+
+$package->addMaintainer(
+ 'lead',
+ 'lcfrs',
+ 'Neuman Vong',
+ 'neuman+pear@twilio.com'
+);
+
+$package->addExtensionDep('required', 'json');
+$package->addExtensionDep('required', 'hash_hmac');
+
+$package->setPhpDep('5.1');
+
+$package->setPearInstallerDep('1.7.0');
+$package->generateContents();
+$package->addRelease();
+
+if ( isset($_GET['make'])
+ || (isset($_SERVER['argv']) && @$_SERVER['argv'][1] == 'make')
+) {
+ $package->writePackageFile();
+} else {
+ $package->debugPackageFile();
+}
+
+?>
diff --git a/package.xml b/package.xml
new file mode 100644
index 0000000..44af9e3
--- /dev/null
+++ b/package.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.9.2" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+ http://pear.php.net/dtd/tasks-1.0.xsd
+ http://pear.php.net/dtd/package-2.0
+ http://pear.php.net/dtd/package-2.0.xsd">
+ <name>JWT</name>
+ <channel>pear.php.net</channel>
+ <summary>A JWT encoder/decoder.</summary>
+ <description>A JWT encoder/decoder.</description>
+ <lead>
+ <name>Neuman Vong</name>
+ <user>lcfrs</user>
+ <email>neuman+pear@twilio.com</email>
+ <active>yes</active>
+ </lead>
+ <date>2011-03-25</date>
+ <time>16:09:40</time>
+ <version>
+ <release>0.0.0</release>
+ <api>0.0.0</api>
+ </version>
+ <stability>
+ <release>alpha</release>
+ <api>alpha</api>
+ </stability>
+ <license uri="http://creativecommons.org/licenses/MIT/">MIT License</license>
+ <notes>
+No release notes.
+ </notes>
+ <contents>
+ <dir baseinstalldir="/" name="/">
+ <dir name="tests">
+ <file name="Bootstrap.php" role="test" />
+ <file name="JWTTest.php" role="test" />
+ <file name="phpunit.xml" role="test" />
+ </dir> <!-- //tests -->
+ <file name="JWT.php" role="php" />
+ <file name="Makefile" role="data" />
+ </dir> <!-- / -->
+ </contents>
+ <dependencies>
+ <required>
+ <php>
+ <min>5.1</min>
+ </php>
+ <pearinstaller>
+ <min>1.7.0</min>
+ </pearinstaller>
+ <extension>
+ <name>json</name>
+ </extension>
+ <extension>
+ <name>hash_hmac</name>
+ </extension>
+ </required>
+ </dependencies>
+ <phprelease />
+ <changelog>
+ <release>
+ <version>
+ <release>0.0.0</release>
+ <api>0.0.0</api>
+ </version>
+ <stability>
+ <release>alpha</release>
+ <api>alpha</api>
+ </stability>
+ <date>2011-03-25</date>
+ <license uri="http://creativecommons.org/licenses/MIT/">MIT License</license>
+ <notes>
+No release notes.
+ </notes>
+ </release>
+ </changelog>
+</package>
diff --git a/tests/Bootstrap.php b/tests/Bootstrap.php
new file mode 100644
index 0000000..235b6d3
--- /dev/null
+++ b/tests/Bootstrap.php
@@ -0,0 +1,9 @@
+<?php
+
+error_reporting(E_ALL | E_STRICT);
+ini_set('display_errors', 1);
+
+$root = realpath(dirname(dirname(__FILE__)));
+require_once $root . '/JWT.php';
+
+unset($root);
diff --git a/tests/JWTTest.php b/tests/JWTTest.php
new file mode 100644
index 0000000..4549f1b
--- /dev/null
+++ b/tests/JWTTest.php
@@ -0,0 +1,31 @@
+<?php
+
+class JWTTests extends PHPUnit_Framework_TestCase {
+ function testEncodeDecode() {
+ $msg = JWT::encode('abc', 'my_key');
+ $this->assertEquals(JWT::decode($msg, 'my_key'), 'abc');
+ }
+
+ function testDecodeFromPython() {
+ $msg = 'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.Iio6aHR0cDovL2FwcGxpY2F0aW9uL2NsaWNreT9ibGFoPTEuMjMmZi5vbz00NTYgQUMwMDAgMTIzIg.E_U8X2YpMT5K1cEiT_3-IvBYfrdIFIeVYeOqre_Z5Cg';
+ $this->assertEquals(
+ JWT::decode($msg, 'my_key'),
+ '*:http://application/clicky?blah=1.23&f.oo=456 AC000 123'
+ );
+ }
+
+ function testUrlSafeCharacters() {
+ $encoded = JWT::encode('f?', 'a');
+ $this->assertEquals('f?', JWT::decode($encoded, 'a'));
+ }
+
+ function testMalformedUtf8StringsFail() {
+ $this->setExpectedException('DomainException');
+ JWT::encode(pack('c', 128), 'a');
+ }
+
+ function testMalformedJsonThrowsException() {
+ $this->setExpectedException('DomainException');
+ JWT::jsonDecode('this is not valid JSON string');
+ }
+}
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
new file mode 100644
index 0000000..ebfe3cf
--- /dev/null
+++ b/tests/phpunit.xml
@@ -0,0 +1,7 @@
+<phpunit bootstrap="./Bootstrap.php">
+ <testsuites>
+ <testsuite name="Services Twilio Test Suite">
+ <directory>./</directory>
+ </testsuite>
+ </testsuites>
+</phpunit>