Initial commit
This commit is contained in:
125
src/Collection/PropertyBag.php
Normal file
125
src/Collection/PropertyBag.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
9
src/Collection/PropertyException.php
Normal file
9
src/Collection/PropertyException.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\SimpleJwt\Collection;
|
||||
|
||||
|
||||
class PropertyException extends \RuntimeException
|
||||
{
|
||||
|
||||
}
|
146
src/JwtToken.php
Normal file
146
src/JwtToken.php
Normal 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
16
src/JwtUtil.php
Normal 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
19
src/Key/JwtDerivedKey.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
19
src/Key/JwtPlaintextKey.php
Normal file
19
src/Key/JwtPlaintextKey.php
Normal 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
9
src/Key/KeyInterface.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\SimpleJwt\Key;
|
||||
|
||||
interface KeyInterface
|
||||
{
|
||||
public function getBinaryKey(): string;
|
||||
}
|
||||
|
9
src/Validator/JwtClaimException.php
Normal file
9
src/Validator/JwtClaimException.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\SimpleJwt\Validator;
|
||||
|
||||
|
||||
class JwtClaimException extends JwtValidatorException
|
||||
{
|
||||
|
||||
}
|
8
src/Validator/JwtHeaderException.php
Normal file
8
src/Validator/JwtHeaderException.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\SimpleJwt\Validator;
|
||||
|
||||
class JwtHeaderException extends JwtValidatorException
|
||||
{
|
||||
|
||||
}
|
8
src/Validator/JwtTokenException.php
Normal file
8
src/Validator/JwtTokenException.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\SimpleJwt\Validator;
|
||||
|
||||
class JwtTokenException extends JwtValidatorException
|
||||
{
|
||||
|
||||
}
|
57
src/Validator/JwtValidator.php
Normal file
57
src/Validator/JwtValidator.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
8
src/Validator/JwtValidatorException.php
Normal file
8
src/Validator/JwtValidatorException.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\SimpleJwt\Validator;
|
||||
|
||||
class JwtValidatorException extends \RuntimeException
|
||||
{
|
||||
|
||||
}
|
Reference in New Issue
Block a user