-WIP on Bearer token

This commit is contained in:
Dave Mc Nicoll 2024-05-31 12:27:07 +00:00
parent 0657622eab
commit c4e4db7a45
13 changed files with 377 additions and 35 deletions

View File

@ -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
{
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Ulmus\User\Authorize\Bearer;
class JsonWebTokenDecodingError extends JsonWebTokenException
{
}

View File

@ -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), '+/', '-_'), '=');
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace Ulmus\User\Authorize\Bearer;
class JsonWebTokenException extends \Exception
{
}

View File

@ -0,0 +1,8 @@
<?php
namespace Ulmus\User\Authorize\Bearer;
class JsonWebTokenInvalidSignatureError extends JsonWebTokenException
{
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -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
{
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Ulmus\User\Authorize\Header;
enum BearerTokenTypeEnum : string
{
case JsonWebToken = "JWT";
case UniqueKey = "UniqueKey";
}

View File

@ -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:

View File

@ -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;
}
}