From c4e4db7a45e9519a608c4619a20256057c0349d6 Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Fri, 31 May 2024 12:27:07 +0000 Subject: [PATCH] -WIP on Bearer token --- src/Authorize/Bearer/Algorithms/HmacSha.php | 15 ++++ .../Bearer/JsonWebTokenAlgorithmEnum.php | 68 +++++++++++++++ src/Authorize/Bearer/JsonWebTokenDecoder.php | 82 +++++++++++++++++++ .../Bearer/JsonWebTokenDecodingError.php | 8 ++ src/Authorize/Bearer/JsonWebTokenEncoder.php | 12 +++ .../Bearer/JsonWebTokenException.php | 8 ++ .../JsonWebTokenInvalidSignatureError.php | 8 ++ src/Authorize/Bearer/JsonWebTokenTypeEnum.php | 18 ++++ src/Authorize/Bearer/JsonWebTokenValidate.php | 56 +++++++++++++ src/Authorize/Header/BearerMethod.php | 46 ++++++++++- src/Authorize/Header/BearerTokenTypeEnum.php | 10 +++ src/Authorize/HeaderAuthentication.php | 8 +- src/Lib/Authenticate.php | 73 +++++++++-------- 13 files changed, 377 insertions(+), 35 deletions(-) create mode 100644 src/Authorize/Bearer/Algorithms/HmacSha.php create mode 100644 src/Authorize/Bearer/JsonWebTokenAlgorithmEnum.php create mode 100644 src/Authorize/Bearer/JsonWebTokenDecoder.php create mode 100644 src/Authorize/Bearer/JsonWebTokenDecodingError.php create mode 100644 src/Authorize/Bearer/JsonWebTokenEncoder.php create mode 100644 src/Authorize/Bearer/JsonWebTokenException.php create mode 100644 src/Authorize/Bearer/JsonWebTokenInvalidSignatureError.php create mode 100644 src/Authorize/Bearer/JsonWebTokenTypeEnum.php create mode 100644 src/Authorize/Bearer/JsonWebTokenValidate.php create mode 100644 src/Authorize/Header/BearerTokenTypeEnum.php diff --git a/src/Authorize/Bearer/Algorithms/HmacSha.php b/src/Authorize/Bearer/Algorithms/HmacSha.php new file mode 100644 index 0000000..dde8c7a --- /dev/null +++ b/src/Authorize/Bearer/Algorithms/HmacSha.php @@ -0,0 +1,15 @@ + $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; + } +} diff --git a/src/Authorize/Bearer/JsonWebTokenDecoder.php b/src/Authorize/Bearer/JsonWebTokenDecoder.php new file mode 100644 index 0000000..34215f2 --- /dev/null +++ b/src/Authorize/Bearer/JsonWebTokenDecoder.php @@ -0,0 +1,82 @@ +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; + } +} \ No newline at end of file diff --git a/src/Authorize/Bearer/JsonWebTokenDecodingError.php b/src/Authorize/Bearer/JsonWebTokenDecodingError.php new file mode 100644 index 0000000..1b18d92 --- /dev/null +++ b/src/Authorize/Bearer/JsonWebTokenDecodingError.php @@ -0,0 +1,8 @@ + $case->name, static::cases()); + } + + public static function exists(string $key) : bool + { + return in_array($key, static::list()); + } +} diff --git a/src/Authorize/Bearer/JsonWebTokenValidate.php b/src/Authorize/Bearer/JsonWebTokenValidate.php new file mode 100644 index 0000000..eb513a6 --- /dev/null +++ b/src/Authorize/Bearer/JsonWebTokenValidate.php @@ -0,0 +1,56 @@ +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(); + } +} \ No newline at end of file diff --git a/src/Authorize/Header/BearerMethod.php b/src/Authorize/Header/BearerMethod.php index 9b627ae..53be05c 100644 --- a/src/Authorize/Header/BearerMethod.php +++ b/src/Authorize/Header/BearerMethod.php @@ -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 + { + } } \ No newline at end of file diff --git a/src/Authorize/Header/BearerTokenTypeEnum.php b/src/Authorize/Header/BearerTokenTypeEnum.php new file mode 100644 index 0000000..8314672 --- /dev/null +++ b/src/Authorize/Header/BearerTokenTypeEnum.php @@ -0,0 +1,10 @@ +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; + } } \ No newline at end of file