Initial commit

This commit is contained in:
2021-02-11 13:22:51 +01:00
commit 3899d191a4
21 changed files with 712 additions and 0 deletions

View File

@ -0,0 +1,125 @@
<?php
namespace NoccyLabs\SimpleJwt\Collection;
use ArrayAccess;
use Countable;
class PropertyBag
{
/**
* @var array
*/
private $props = [];
public function __construct(array $props=[])
{
$this->props = $props;
}
/**
* Add a property value, fails if the property exists
*
* @param string Property name
* @param mixed Value
* @throws PropertyException if the property already exists
*/
public function add(string $prop, $value)
{
if (array_key_exists($prop, $this->props)) {
throw new PropertyException("Property already exists");
}
$this->props[$prop] = $value;
}
/**
* Set a property value, create the property if it doesn't
* exist.
*
* @param string Property name
* @param mixed Value
*/
public function set(string $prop, $value)
{
$this->props[$prop] = $value;
}
/**
* Get the value of a property, fails if the property does not exist.
* Use the value() method to get with a default value
*
* @param string Property name
* @return mixed
* @throws PropertyException if the property does not exist
*/
public function get(string $prop)
{
if (!array_key_exists($prop, $this->props)) {
throw new PropertyException("No such property");
}
return $this->props[$prop];
}
/**
* Retrieve all properties.
*
* @return array
*/
public function getAll(): array
{
return $this->props;
}
/**
* Retrieve all properties as a json string
*
* @return string
*/
public function getJson(): string
{
return json_encode($this->props, JSON_UNESCAPED_SLASHES);
}
/**
* Get the value of the property, or use the provided default value.
*
* @param string Property name
* @param mixed Default value
* @return mixed
*/
public function value(string $prop, $default=null)
{
return array_key_exists($prop, $this->props)
? $this->props[$prop]
: $default;
}
/**
* Remove a property
*/
public function delete(string $prop)
{
unset($this->props[$prop]);
}
/**
* Check if a property is present
*/
public function has(string $prop): bool
{
return array_key_exists($prop, $this->props);
}
/**
* Check if all the provided properties are present
*/
public function hasAll(array $props)
{
foreach ($props as $prop) {
if (!$this->has($prop)) return false;
}
return true;
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace NoccyLabs\SimpleJwt\Collection;
class PropertyException extends \RuntimeException
{
}

146
src/JwtToken.php Normal file
View File

@ -0,0 +1,146 @@
<?php
namespace NoccyLabs\SimpleJwt;
use NoccyLabs\SimpleJwt\Collection\PropertyBag;
use NoccyLabs\SimpleJwt\Key\KeyInterface;
/**
*
*
*
*
* @property-read header PropertyBag
* @property-read claim PropertyBag
*/
class JwtToken
{
/** @var PropertyBag */
private $header;
/** @var PropertyBag */
private $claims;
/** @var KeyInterface */
private $key;
/** @var bool */
private $valid;
/** @var bool */
private $generated;
/**
* Constructor
*
*
* @param KeyInterface The key used to sign the token
*/
public function __construct(KeyInterface $key, ?string $token=null)
{
$this->key = $key;
if ($token) {
$this->parseToken($token);
} else {
$this->header = new PropertyBag([
'alg' => "HS256",
'typ' => "JWT",
]);
$this->claims = new PropertyBag();
$this->valid = true;
$this->generated = true;
}
}
private function parseToken(string $token)
{
$this->generated = false;
[ $header, $payload, $signature ] = explode(".", trim($token), 3);
$hash = JwtUtil::encode(hash_hmac("sha256", $header.".".$payload, $this->key->getBinaryKey(), true));
if ($signature == $hash) {
$this->valid = true;
}
$this->header = new PropertyBag(json_decode(JwtUtil::decode($header), true));
$this->claims = new PropertyBag(json_decode(JwtUtil::decode($payload), true));
if ($this->header->has('exp')) {
$exp = intval($this->header->get('exp'));
if ($exp <= time()) {
// Invalid if expired
$this->valid = false;
}
}
}
public function isValid(): bool
{
return $this->valid;
}
public function isGenerated(): bool
{
return $this->generated;
}
public function __get(string $key)
{
switch ($key) {
case 'header': return $this->header;
case 'claims': return $this->claims;
}
}
public function addClaim(string $name, $value)
{
$this->claims->add($name, $value);
}
public function setClaim(string $name, $value)
{
$this->claims->set($name, $value);
}
public function setExpiry($expiry)
{
if ($expiry instanceof \DateTime) {
$this->header->set('exp', $expiry->format("U"));
} elseif ($expiry === null) {
$this->header->delete('exp');
} elseif (is_numeric($expiry)) {
if ($expiry < time()) {
$this->header->set('exp', time() + $expiry);
} else {
$this->header->set('exp', intval($expiry));
}
} elseif ($t = strtotime($expiry)) {
$this->header->set('exp', $t);
} elseif (preg_match('/([0-9]+)([wdhm])/', $expiry, $match)) {
switch ($match[2]) {
case 'w':
$fact = 60 * 60 * 24 * 7;
break;
case 'd':
$fact = 60 * 60 * 24;
break;
case 'h':
$fact = 60 * 60;
break;
case 'm':
$fact = 60;
break;
}
$this->header->set('exp', time() + (intval($match[1]) * $fact));
} else {
throw new \InvalidArgumentException();
}
}
public function getSignedToken(): string
{
$header = JwtUtil::encode($this->header->getJson());
$payload = JwtUtil::encode($this->claims->getJson());
$hash = JwtUtil::encode(hash_hmac("sha256", $header.".".$payload, $this->key->getBinaryKey(), true));
return $header.".".$payload.".".$hash;
}
}

16
src/JwtUtil.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace NoccyLabs\SimpleJwt;
class JwtUtil
{
public static function encode($data) {
return rtrim(str_replace(['+', '/'], ['-', '_'], base64_encode($data)), "=");
}
public static function decode($data) {
return base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
}
}

19
src/Key/JwtDerivedKey.php Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace NoccyLabs\SimpleJwt\Key;
class JwtDerivedKey implements KeyInterface
{
private $key;
public function __construct(string $password, string $salt, int $iter=100)
{
$this->key = hash_pbkdf2("sha256", $password, $salt, $iter, 0, true);
}
public function getBinaryKey(): string
{
return $this->key;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace NoccyLabs\SimpleJwt\Key;
class JwtPlaintextKey implements KeyInterface
{
private $key;
public function __construct(string $key)
{
$this->key = $key;
}
public function getBinaryKey(): string
{
return $this->key;
}
}

9
src/Key/KeyInterface.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace NoccyLabs\SimpleJwt\Key;
interface KeyInterface
{
public function getBinaryKey(): string;
}

View File

@ -0,0 +1,9 @@
<?php
namespace NoccyLabs\SimpleJwt\Validator;
class JwtClaimException extends JwtValidatorException
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\SimpleJwt\Validator;
class JwtHeaderException extends JwtValidatorException
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\SimpleJwt\Validator;
class JwtTokenException extends JwtValidatorException
{
}

View File

@ -0,0 +1,57 @@
<?php
namespace NoccyLabs\SimpleJwt\Validator;
use NoccyLabs\SimpleJwt\JwtToken;
use NoccyLabs\SimpleJwt\Key\KeyInterface;
class JwtValidator
{
private $requireHeaders = [];
private $requireClaims = [];
public function __construct()
{
$this->requireHeaders = [
'alg',
'typ',
];
}
public function addRequiredClaim(string $name)
{
$this->requireClaims[$name] = true;
}
public function addRequiredClaimWithValue(string $name, $value)
{
$this->requireClaims[$name] = [ $value ];
}
public function validateToken(JwtToken $token)
{
if (!$token->isValid()) {
throw new JwtTokenException("The token is not valid");
}
if (!$token->header->hasAll($this->requireHeaders)) {
throw new JwtHeaderException("The token is missing one or more required headers");
}
if (!$token->claims->hasAll($this->requireClaims)) {
throw new JwtHeaderException("The token is missing one or more required claims");
}
return true;
}
public function validate(KeyInterface $key, string $raw)
{
$token = new JwtToken($key, $raw);
if ($this->validateToken($token)) {
return $token;
}
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace NoccyLabs\SimpleJwt\Validator;
class JwtValidatorException extends \RuntimeException
{
}