Skip to content

Commit

Permalink
Allow for custom properties in JWS header
Browse files Browse the repository at this point in the history
* Switch JWS constructor to accept header as array
* Includes bump to major version 4 in README
* Move auto claim handling into new EasyJWS class
  • Loading branch information
brianjmiller committed Apr 21, 2015
1 parent 4278a06 commit c2e29fd
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 97 deletions.
34 changes: 21 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ implementation of the JWS

This library needs PHP 5.4+ and the library OpenSSL.

It has been tested using `PHP5.3` to `PHP5.6` and `HHVM`.
It has been tested using `PHP5.4` to `PHP5.6` and `HHVM`.


## Installation
Expand All @@ -22,7 +22,7 @@ You can install the library directly from
composer / [packagist](https://packagist.org/packages/namshi/jose):

```
"namshi/jose": "2.1.*"
"namshi/jose": "4.0.*"
```

## Usage
Expand All @@ -43,12 +43,14 @@ First, generate the JWS:
``` php
<?php

use Namshi\JOSE\JWS;
use Namshi\JOSE\EasyJWS;

if ($username == 'correctUsername' && $pass == 'ok') {
$user = Db::loadUserByUsername($username);

$jws = new JWS('RS256');
$jws = new EasyJWS(array(
'alg' => 'RS256'
));
$jws->setPayload(array(
'uid' => $user->getid(),
));
Expand All @@ -68,9 +70,9 @@ is a valid call:
``` php
<?php

use Namshi\JOSE\JWS;
use Namshi\JOSE\EasyJWS;

$jws = JWS::load($_COOKIE['identity']);
$jws = EasyJWS::load($_COOKIE['identity']);
$public_key = openssl_pkey_get_public("/path/to/public.key");

// verify that the token is valid and had the same values
Expand Down Expand Up @@ -98,12 +100,12 @@ In these cases, simply add the optional `'SecLib'` parameter when
constructing a JWS:

```php
$jws = new JWS('RS256', 'JWS', 'SecLib');
$jws = new JWS(array('alg' => 'RS256'), 'SecLib');
```

You can now use the PHPSecLib implmentaiton of RSA signing. If you use
You can now use the PHPSecLib implementation of RSA signing. If you use
a password protected private key, you can still submit the private key
to use for signing as a string, as long as if you pass the password as the
to use for signing as a string, as long as you pass the password as the
second parameter into the `sign` method:

```php
Expand All @@ -118,15 +120,15 @@ $jws = JWS::load($tokenString, false, $encoder, 'SecLib');

## Under the hood

In order to [validate the JWS](https://github.com/namshi/jose/blob/master/src/Namshi/JOSE/JWS.php#L126),
the signature is first [verified](https://github.com/namshi/jose/blob/master/src/Namshi/JOSE/JWS.php#L110)
with a public key and then we will check whether the [token is expired](https://github.com/namshi/jose/blob/master/src/Namshi/JOSE/JWS.php#L172).
In order to [validate the JWS](https://github.com/namshi/jose/blob/master/src/Namshi/JOSE/EasyJWS.php#L43),
the signature is first [verified](https://github.com/namshi/jose/blob/master/src/Namshi/JOSE/JWS.php#L113)
with a public key and then we will check whether the [token is expired](https://github.com/namshi/jose/blob/master/src/Namshi/JOSE/EasyJWS.php#L55).

To give a JWS a TTL, just use the standard `exp` value in the payload:

``` php
$date = new DateTime('tomorrow');
$this->jws = new JWS('RS256');
$this->jws = new EasyJWS(array('alg' => 'RS256'));
$this->jws->setPayload(array(
'exp' => $date->format('U'),
));
Expand Down Expand Up @@ -154,6 +156,12 @@ If, for some reason, you need to encode the token in a different way, you can
inject any implementation of `Namshi\JOSE\Base64\Encoder` in a `JWS` instance.
Likewise, `JWS::load()` accepts such an implementation as a second argument.

## Implementation Specifics

The library provides a base JWT Class that implements what is needed just for JSON Web Tokens. The JWS Class then extends
the JWT class and adds the implementation for signing and verifying using JSON Web Signatures. The EasyJWS class extends
the base JWS class and adds validation of a TTL and inclusion of automatic claims.

## Credits

This library has been inspired by the
Expand Down
73 changes: 73 additions & 0 deletions src/Namshi/JOSE/EasyJWS.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Namshi\JOSE;

/**
* Class providing an easy to use JWS implementation.
*/
class EasyJWS extends JWS
{
/**
* Constructor
*
* @param array $header An associative array of headers. The value can be any type accepted by json_encode or a JSON serializable object
* @see http://php.net/manual/en/function.json-encode.php
* @see http://php.net/manual/en/jsonserializable.jsonserialize.php
* @see https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-4
* }
*/
public function __construct($header = array())
{
if (!isset($header['typ'])) {
$header['typ'] = 'JWS';
}
parent::__construct($header);
}

/**
* Sets the payload of the current JWS with an issued at value in the 'iat' property.
*
* @param array $payload
*/
public function setPayload(array $payload)
{
if (!isset($payload['iat'])) {
$now = new \DateTime('now');
$payload['iat'] = $now->format('U');
}

return parent::setPayload($payload);
}

/**
* Checks that the JWS has been signed with a valid private key by verifying it with a public $key
* and the token is not expired.
*
* @param resource|string $key
* @param string $algo The algorithms this JWS should be signed with. Use it if you want to restrict which algorithms you want to allow to be validated.
*
* @return bool
*/
public function isValid($key, $algo = null)
{
return $this->verify($key, $algo) && ! $this->isExpired();
}

/**
* Checks whether the token is expired based on the 'exp' value.
*
* @return bool
*/
protected function isExpired()
{
$payload = $this->getPayload();

if (isset($payload['exp']) && is_numeric($payload['exp'])) {
$now = new \DateTime('now');

return ($now->format('U') - $payload['exp']) > 0;
}

return false;
}
}
50 changes: 11 additions & 39 deletions src/Namshi/JOSE/JWS.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use Namshi\JOSE\Base64\Encoder;

/**
* Class representing a JSOn Web Signature.
* Class representing a JSON Web Signature.
*/
class JWS extends JWT
{
Expand All @@ -22,17 +22,21 @@ class JWS extends JWT
/**
* Constructor
*
* @param string $algorithm
* @param string $type
* @param array $header An associative array of headers. The value can be any type accepted by json_encode or a JSON serializable object
* @see http://php.net/manual/en/function.json-encode.php
* @see http://php.net/manual/en/jsonserializable.jsonserialize.php
* @see https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-41#section-4
* @param string $encryptionEngine
* }
*/
public function __construct($algorithm, $type = null, $encryptionEngine = "OpenSSL")
public function __construct($header = array(), $encryptionEngine = "OpenSSL")
{
if (!in_array($encryptionEngine, $this->supportedEncryptionEngines)) {
throw new InvalidArgumentException(sprintf("Encryption engine %s is not supported", $encryptionEngine));
}
$this->encryptionEngine = $encryptionEngine;
parent::__construct(array(), array('alg' => $algorithm, 'typ' => $type ?: "JWS"));

parent::__construct(array(), $header);
}

/**
Expand Down Expand Up @@ -110,8 +114,8 @@ public static function load($jwsTokenString, $allowUnsecure = false, Encoder $en
throw new InvalidArgumentException(sprintf('The token "%s" cannot be validated in a secure context, as it uses the unallowed "none" algorithm', $jwsTokenString));
}

$jws = new self($header['alg'], isset($header['typ']) ? $header['typ'] : null, $encryptionEngine);
$jws = new static($header, $encryptionEngine);

$jws->setEncoder($encoder)
->setHeader($header)
->setPayload($payload)
Expand Down Expand Up @@ -144,20 +148,6 @@ public function verify($key, $algo = null)
return $this->getSigner()->verify($key, $decodedSignature, $signinInput);
}

/**
* Checks that the JWS has been signed with a valid private key by verifying it with a public $key
* and the token is not expired.
*
* @param resource|string $key
* @param string $algo The algorithms this JWS should be signed with. Use it if you want to restrict which algorithms you want to allow to be validated.
*
* @return bool
*/
public function isValid($key, $algo = null)
{
return $this->verify($key, $algo) && ! $this->isExpired();
}

/**
* Returns the base64 encoded signature.
*
Expand Down Expand Up @@ -198,22 +188,4 @@ protected function getSigner()
throw new InvalidArgumentException(
sprintf("The algorithm '%s' is not supported for %s", $this->header['alg'], $this->encryptionEngine));
}

/**
* Checks whether the token is expired.
*
* @return bool
*/
protected function isExpired()
{
$payload = $this->getPayload();

if (isset($payload['exp']) && is_numeric($payload['exp'])) {
$now = new \DateTime('now');

return ($now->format('U') - $payload['exp']) > 0;
}

return false;
}
}
11 changes: 3 additions & 8 deletions src/Namshi/JOSE/JWT.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ class JWT
*/
public function __construct(array $payload, array $header)
{
$this->payload = $payload;
$this->header = $header;
$this->encoder = new Base64UrlSafeEncoder();
$this->setPayload($payload);
$this->setHeader($header);
$this->setEncoder(new Base64UrlSafeEncoder());
}

/**
Expand Down Expand Up @@ -81,11 +81,6 @@ public function setPayload(array $payload)
{
$this->payload = $payload;

if (!isset($this->payload['iat'])) {
$now = new \DateTime('now');
$this->payload['iat'] = $now->format('U');
}

return $this;
}

Expand Down
2 changes: 1 addition & 1 deletion tests/Namshi/JOSE/Test/BCJWSTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function testTestBC()
);

foreach ($data as $payload) {
$jwsOld = new JWS("RS256");
$jwsOld = new JWS(array("alg" => "RS256"));
$jwsOld->setEncoder(new Base64Encoder());
$jwsOld->setPayload($payload);
$jwsOld->sign(openssl_pkey_get_private(SSL_KEYS_PATH . "private.key", self::SSL_KEY_PASSPHRASE));
Expand Down
53 changes: 53 additions & 0 deletions tests/Namshi/JOSE/Test/EasyJWSTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace Namshi\JOSE\Test;

use PHPUnit_Framework_TestCase as TestCase;
use Namshi\JOSE\EasyJWS;
use DateTime;

class EasyJWSTest extends TestCase
{
const SSL_KEY_PASSPHRASE = 'tests';

public function setup()
{
$date = new DateTime('tomorrow');
$data = array(
'a' => 'b',
'exp' => $date->format('U')
);
$this->jws = new EasyJWS(array('alg' => 'RS256'));
$this->jws->setPayload($data);
}

public function testConstruction()
{
$this->assertSame($this->jws->getHeader(), array('alg' => 'RS256', 'typ' => 'JWS'));
$this->assertRegExp('/^\d+$/', $this->jws->getPayload()['iat'], 'iat property has integer value (from construction)');
}

public function testValidationOfAValidEasyJWS()
{
$privateKey = openssl_pkey_get_private(SSL_KEYS_PATH . "private.key", self::SSL_KEY_PASSPHRASE);
$this->jws->sign($privateKey);

$jws = EasyJWS::load($this->jws->getTokenString());
$public_key = openssl_pkey_get_public(SSL_KEYS_PATH . "public.key");
$this->assertTrue($jws->isValid($public_key, 'RS256'));
}

public function testValidationOfInvalidEasyJWS()
{
$date = new DateTime('yesterday');
$this->jws->setPayload(array(
'exp' => $date->format('U')
));
$privateKey = openssl_pkey_get_private(SSL_KEYS_PATH . "private.key", self::SSL_KEY_PASSPHRASE);
$this->jws->sign($privateKey);

$jws = EasyJWS::load($this->jws->getTokenString());
$public_key = openssl_pkey_get_public(SSL_KEYS_PATH . "public.key");
$this->assertFalse($jws->isValid($public_key, 'RS256'));
}
}
Loading

0 comments on commit c2e29fd

Please sign in to comment.