-WIP on Bearer token
This commit is contained in:
parent
0657622eab
commit
c4e4db7a45
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Bearer\Algorithms;
|
||||
|
||||
use Ulmus\User\Authorize\Bearer\JsonWebTokenException;
|
||||
|
||||
class HmacSha
|
||||
{
|
||||
public static function encode(string $encodedHeader, string $encodedPayload, string $secretKey) : string
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Bearer;
|
||||
|
||||
enum JsonWebTokenAlgorithmEnum
|
||||
{
|
||||
case HS256;
|
||||
|
||||
case HS384;
|
||||
|
||||
case HS512;
|
||||
|
||||
case RS256;
|
||||
|
||||
case RS384;
|
||||
|
||||
case RS512;
|
||||
|
||||
case ES256;
|
||||
|
||||
case ES384;
|
||||
|
||||
case ES512;
|
||||
|
||||
case PS256;
|
||||
|
||||
case PS384;
|
||||
|
||||
case PS512;
|
||||
|
||||
public static function list() : array
|
||||
{
|
||||
return array_map(fn(JsonWebTokenAlgorithmEnum $case) => $case->name, static::cases());
|
||||
}
|
||||
|
||||
public static function fromString(string $name) : self
|
||||
{
|
||||
foreach (static::cases() as $item) {
|
||||
if ($item->name === $name) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function exists(string $key) : bool
|
||||
{
|
||||
return in_array($key, static::list());
|
||||
}
|
||||
|
||||
public function assessOperability() : void
|
||||
{
|
||||
$method = $this->phpAlgoMethods();
|
||||
|
||||
if (empty($method) || ! in_array($method[0], $method[2])) {
|
||||
throw new JsonWebTokenException(sprintf("Choosen algorithm do not seems to be supported by your PHP version '%s'", $this->name));
|
||||
}
|
||||
}
|
||||
|
||||
public function phpAlgoMethods() : false|array
|
||||
{
|
||||
return match($this) {
|
||||
self::HS256 => [ 'sha256', 'hash_hmac', hash_hmac_algos() ],
|
||||
self::HS384 => [ 'sha384', 'hash_hmac', hash_hmac_algos() ],
|
||||
self::HS512 => [ 'sha512', 'hash_hmac', hash_hmac_algos() ],
|
||||
# Support for other algorithms not implemented yet...
|
||||
} ?? false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Bearer;
|
||||
|
||||
class JsonWebTokenDecoder
|
||||
{
|
||||
protected array $header;
|
||||
|
||||
protected array $payload;
|
||||
|
||||
protected JsonWebTokenAlgorithmEnum $algrithm;
|
||||
|
||||
public function __construct(
|
||||
public string $encoded
|
||||
) {}
|
||||
|
||||
protected function parse() : bool
|
||||
{
|
||||
try {
|
||||
list($encodedHeader, $encodedPayload, $signature) = explode('.', $this->encoded);
|
||||
|
||||
foreach([ 'header' => $encodedHeader, 'payload' => $encodedPayload ] as $key => $value) {
|
||||
$decoded = static::base64url_decode($value) ;
|
||||
|
||||
if ( $decoded !== false ){
|
||||
$jsonArray = json_decode($decoded, true);
|
||||
|
||||
if ( is_array($jsonArray) ) {
|
||||
if ($key === 'header') {
|
||||
JsonWebTokenValidate::validateHeaderType($jsonArray);
|
||||
JsonWebTokenValidate::validateHeaderAlgorithm($jsonArray);
|
||||
}
|
||||
|
||||
$this->$key = $jsonArray;
|
||||
}
|
||||
else {
|
||||
throw new JsonWebTokenDecodingError(sprintf("Invalid JSON returned while decoding section %s ; an array must be provided", $key));
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new JsonWebTokenDecodingError(sprintf("An error occured while decoding a base64 string from section %s", $key));
|
||||
}
|
||||
}
|
||||
|
||||
JsonWebTokenValidate::validateSignature($this->header['alg'], getenv('LEAN_RANDOM'), $encodedHeader, $encodedPayload, $signature);
|
||||
}
|
||||
catch(\Throwable $t) {
|
||||
throw new JsonWebTokenDecodingError($t->getMessage(), $t->getCode(), $t);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function base64url_decode($data) : string|false
|
||||
{
|
||||
return base64_decode(strtr($data, '-_', '+/'));
|
||||
}
|
||||
|
||||
public function decode() : bool
|
||||
{
|
||||
return $this->parse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic validation of JWT encoded string
|
||||
* @return bool
|
||||
*/
|
||||
public function isJWT() : bool
|
||||
{
|
||||
try {
|
||||
return $this->parse();
|
||||
}
|
||||
catch(\Throwable $t) {}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getPayload() : array
|
||||
{
|
||||
return $this->payload;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Bearer;
|
||||
|
||||
class JsonWebTokenDecodingError extends JsonWebTokenException
|
||||
{
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Bearer;
|
||||
|
||||
class JsonWebTokenEncoder
|
||||
{
|
||||
|
||||
public static function base64url_encode($data) : string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Bearer;
|
||||
|
||||
class JsonWebTokenException extends \Exception
|
||||
{
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Bearer;
|
||||
|
||||
class JsonWebTokenInvalidSignatureError extends JsonWebTokenException
|
||||
{
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Bearer;
|
||||
|
||||
enum JsonWebTokenTypeEnum
|
||||
{
|
||||
case JWT;
|
||||
|
||||
public static function list() : array
|
||||
{
|
||||
return array_map(fn(JsonWebTokenTypeEnum $case) => $case->name, static::cases());
|
||||
}
|
||||
|
||||
public static function exists(string $key) : bool
|
||||
{
|
||||
return in_array($key, static::list());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Bearer;
|
||||
|
||||
abstract class JsonWebTokenValidate
|
||||
{
|
||||
public static function validateHeaderAlgorithm(array $header) : void
|
||||
{
|
||||
if (! array_key_exists('alg', $header)) {
|
||||
throw new JsonWebTokenDecodingError("Your header data is missing a valid algorithm (alg).");
|
||||
}
|
||||
|
||||
if ( ! JsonWebTokenAlgorithmEnum::exists($header['alg']) ) {
|
||||
throw new JsonWebTokenDecodingError(
|
||||
sprintf("Given algorithm '%s' is not supported. Please try with one of the above : %s", $header['alg'], implode(', ', JsonWebTokenAlgorithmEnum::list()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function validateHeaderType(array $header) : void
|
||||
{
|
||||
if ( array_key_exists('typ', $header) && ! JsonWebTokenTypeEnum::exists($header['typ']) ) {
|
||||
throw new JsonWebTokenDecodingError(
|
||||
sprintf("Given type '%s' is not supported. Please try with one of the above : %s", $header['typ'], implode(', ', JsonWebTokenTypeEnum::list()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function validateSignature(string $alg, string $secret, string $encodedHeader, string $encodedPayload, string $encodedSignature) : void
|
||||
{
|
||||
$algorithm = JsonWebTokenAlgorithmEnum::fromString($alg);
|
||||
|
||||
static::validateAlgorithm($algorithm);
|
||||
|
||||
$decodedSignature = JsonWebTokenDecoder::base64url_decode($encodedSignature);
|
||||
|
||||
list($algo, $method, ) = $algorithm->phpAlgoMethods();
|
||||
|
||||
switch($method) {
|
||||
case 'hash_hmac':
|
||||
$compare = hash_hmac($algo, sprintf("%s.%s", $encodedHeader, $encodedPayload), $secret, true);
|
||||
break;
|
||||
}
|
||||
|
||||
if ( ($compare ?? null) !== $decodedSignature) {
|
||||
throw new JsonWebTokenDecodingError(
|
||||
sprintf("Given signature (%s) do not match computed signature (%s)", $encodedSignature, JsonWebTokenEncoder::base64url_encode($compare))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function validateAlgorithm(JsonWebTokenAlgorithmEnum $algorithm) : void
|
||||
{
|
||||
$algorithm->assessOperability();
|
||||
}
|
||||
}
|
|
@ -3,10 +3,15 @@
|
|||
namespace Ulmus\User\Authorize\Header;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Ulmus\User\Authorize\Bearer\JsonWebToken;
|
||||
use Ulmus\User\Authorize\Bearer\JsonWebTokenDecoder;
|
||||
use Ulmus\User\Entity\UserInterface;
|
||||
use Ulmus\User\Lib\Authorize;
|
||||
|
||||
class BearerMethod implements MethodInterface
|
||||
{
|
||||
protected JsonWebTokenDecoder $jwt;
|
||||
|
||||
public function __construct(
|
||||
protected UserInterface $user,
|
||||
protected string $token
|
||||
|
@ -14,6 +19,45 @@ class BearerMethod implements MethodInterface
|
|||
|
||||
public function execute(ServerRequestInterface $request) : bool
|
||||
{
|
||||
return false;
|
||||
switch($this->autodetectTokenType()) {
|
||||
case BearerTokenTypeEnum::JsonWebToken:
|
||||
$authorize = new Authorize($this->user);
|
||||
|
||||
$payload = $this->jwt->getPayload();
|
||||
|
||||
if ($payload['sub'] ?? false) {
|
||||
if ( ! $authorize->logUser((int) $payload['sub']) ) {
|
||||
throw new \Exception("Given user id do not match with an existing/active user");
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new \InvalidArgumentException("Given JsonWebToken is missing a 'sub' key (which concords to user id)");
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case BearerTokenTypeEnum::UniqueKey:
|
||||
# @TODO
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
return $this->user->loggedIn();
|
||||
}
|
||||
|
||||
public function autodetectTokenType() : BearerTokenTypeEnum
|
||||
{
|
||||
$this->jwt = new JsonWebTokenDecoder($this->token);
|
||||
|
||||
if ( $this->jwt->isJWT() ) {
|
||||
return BearerTokenTypeEnum::JsonWebToken;
|
||||
}
|
||||
|
||||
return BearerTokenTypeEnum::UniqueKey;
|
||||
}
|
||||
|
||||
protected function userFromJWT() : UserInterface
|
||||
{
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\User\Authorize\Header;
|
||||
|
||||
enum BearerTokenTypeEnum : string
|
||||
{
|
||||
case JsonWebToken = "JWT";
|
||||
|
||||
case UniqueKey = "UniqueKey";
|
||||
}
|
|
@ -23,11 +23,15 @@ class HeaderAuthentication implements AuthorizeMethodInterface
|
|||
throw new \RuntimeException("Your user entity must provide a valid hash of `user:realm:password` ");
|
||||
}
|
||||
|
||||
$method = new Header\DigestMethod($user, $value);
|
||||
$methodObj = new Header\DigestMethod($user, $value);
|
||||
break;
|
||||
|
||||
case "bearer":
|
||||
$method = new Header\BearerMethod($user, $value);
|
||||
$methodObj = new Header\BearerMethod($user, $value);
|
||||
break;
|
||||
|
||||
case "token":
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
|
|
|
@ -26,29 +26,12 @@ class Authenticate {
|
|||
|
||||
public function rememberMe() : ? UserInterface
|
||||
{
|
||||
$logUser = function(? int $id) {
|
||||
try {
|
||||
if ($id === null || null === ($user = $this->user::repository()->loadFromPk($id))) {
|
||||
throw new \InvalidArgumentException(sprintf("User having id '%s' was not found.", $id));
|
||||
}
|
||||
}
|
||||
catch(\Exception $ex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->user->fromArray($user);
|
||||
|
||||
$this->user->logged = true;
|
||||
|
||||
return $this->user;
|
||||
};
|
||||
|
||||
if ( $this->session && $this->session->has("user.id") ) {
|
||||
return $logUser($this->session->get("user.id"));
|
||||
return $this->logUser($this->session->get("user.id"));
|
||||
}
|
||||
|
||||
if ( $this->cookie && $this->cookie->has("user.id") ) {
|
||||
return $logUser($this->cookie->get("user.id"));
|
||||
return $this->logUser($this->cookie->get("user.id"));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -86,15 +69,7 @@ class Authenticate {
|
|||
$response = call_user_func_array($this->authenticationEvent, [ false, 'verifyPassword', $this->user, [ 'password' => $password ] ]);
|
||||
|
||||
if ( $response !== null ? $response : $this->user->verifyPassword($password) ) {
|
||||
$this->user->logged = true;
|
||||
|
||||
if ( $this->session ) {
|
||||
$this->session->set("user.id", $this->user->id);
|
||||
}
|
||||
|
||||
if ( $this->cookie ) {
|
||||
$this->cookie->set("user.id", $this->user->id);
|
||||
}
|
||||
$this->login();
|
||||
|
||||
call_user_func_array($this->authenticationEvent, [ true, 'success', $this->user ]);
|
||||
}
|
||||
|
@ -112,8 +87,28 @@ class Authenticate {
|
|||
return $this->user->logged;
|
||||
}
|
||||
|
||||
|
||||
public function login() : void
|
||||
{
|
||||
if ( !$this->user->isLoaded() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->user->logged = true;
|
||||
|
||||
if ( $this->session ) {
|
||||
$this->session->set("user.id", $this->user->id);
|
||||
}
|
||||
|
||||
if ( $this->cookie ) {
|
||||
$this->cookie->set("user.id", $this->user->id);
|
||||
}
|
||||
}
|
||||
|
||||
public function logout() : self
|
||||
{
|
||||
$this->user->logged = false;
|
||||
|
||||
if ( $this->session ) {
|
||||
$this->session->delete('user.id');
|
||||
}
|
||||
|
@ -122,10 +117,24 @@ class Authenticate {
|
|||
$this->cookie->delete('user.id');
|
||||
}
|
||||
|
||||
if ( isset($this->user) ) {
|
||||
$this->user->logged = false;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function logUser(?int $id) : ? UserInterface
|
||||
{
|
||||
try {
|
||||
if ($id === null || null === ($entity = $this->user::repository()->loadFromPk($id))) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch(\Exception $ex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->user->fromArray($entity);
|
||||
|
||||
$this->user->logged = true;
|
||||
|
||||
return $this->user;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue