From 6582378b6e39125e287b9794626c0f8e0a40bc01 Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Fri, 28 Mar 2025 13:38:28 +0000 Subject: [PATCH 1/3] - WIP on 2.x auth. method --- src/Authorize/HeaderAuthentication.php | 1 - src/Lib/Authenticate.php | 15 ++++-- src/Lib/AuthenticationMethodEnum.php | 9 ++++ src/Middleware/AuthenticationMiddleware.php | 46 +++++++++++++++++++ .../HeaderAuthenticationMiddleware.php | 8 ++-- 5 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 src/Lib/AuthenticationMethodEnum.php create mode 100644 src/Middleware/AuthenticationMiddleware.php diff --git a/src/Authorize/HeaderAuthentication.php b/src/Authorize/HeaderAuthentication.php index 7eb4d45..c0a6323 100644 --- a/src/Authorize/HeaderAuthentication.php +++ b/src/Authorize/HeaderAuthentication.php @@ -42,7 +42,6 @@ class HeaderAuthentication implements AuthorizeMethodInterface return isset($methodObj) && $methodObj->execute($request); } - public function catchRequest(ServerRequestInterface $request) : bool { foreach(array_merge($request->getHeader('Accept'), $request->getHeader('Content-Type')) as $accept) { diff --git a/src/Lib/Authenticate.php b/src/Lib/Authenticate.php index 2692bd3..fe00b39 100644 --- a/src/Lib/Authenticate.php +++ b/src/Lib/Authenticate.php @@ -120,7 +120,7 @@ class Authenticate { return $this; } - public function logUser(?int $id) : ? UserInterface + public function loadUser(?int $id) : ? UserInterface { try { if ($id === null || null === ($entity = $this->user::repository()->loadFromPk($id))) { @@ -133,8 +133,17 @@ class Authenticate { $this->user->fromArray($entity); - $this->user->loggedIn(true); - return $this->user; } + + public function logUser(?int $id) : ? UserInterface + { + if ( null !== $this->loadUser($id) ) { + $this->user->loggedIn(true); + + return $this->user; + } + + return null; + } } \ No newline at end of file diff --git a/src/Lib/AuthenticationMethodEnum.php b/src/Lib/AuthenticationMethodEnum.php new file mode 100644 index 0000000..afe3bfd --- /dev/null +++ b/src/Lib/AuthenticationMethodEnum.php @@ -0,0 +1,9 @@ +getAttribute('authentication_middleware:user_id')) { + $this->authenticator->loadUser($id); + } + + switch($request->getAttribute('authentication_middleware:method')) { + case AuthenticationMethodEnum::ForceLogin: + $this->authenticator->login(); + break; + + case AuthenticationMethodEnum::UsernamePassword: + $this->authenticator->authenticate($request->getAttribute('authentication_middleware:username'), $request->getAttribute('authentication_middleware:password')); + break; + } + } + catch(\Exception $e) { + throw $e; + } + + return $handler->handle($request); + } +} diff --git a/src/Middleware/HeaderAuthenticationMiddleware.php b/src/Middleware/HeaderAuthenticationMiddleware.php index e97f270..7f54a41 100644 --- a/src/Middleware/HeaderAuthenticationMiddleware.php +++ b/src/Middleware/HeaderAuthenticationMiddleware.php @@ -13,15 +13,13 @@ use Ulmus\User\Authorize\HeaderAuthentication; class HeaderAuthenticationMiddleware implements MiddlewareInterface { - protected HeaderAuthentication $authenticator; + #protected HeaderAuthentication $authenticator; public function __construct( protected UserInterface $entity, protected \Closure $loginFailedResponse, - HeaderAuthentication $authenticator = null, - ) { - $this->authenticator = $authenticator ?: new HeaderAuthentication(); - } + protected HeaderAuthentication $authenticator, + ) { } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { From 0eaaf79ad80b27d8f7fa88f287b25087e3652cd9 Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Wed, 25 Feb 2026 20:10:09 +0000 Subject: [PATCH 2/3] - WIP on v2.x authentication --- src/Authorize/Bearer/JsonWebTokenDecoder.php | 8 ++-- src/Authorize/Header/BearerMethod.php | 10 ++++- src/Authorize/HeaderAuthentication.php | 2 +- src/Entity/User.php | 6 +-- src/Exception/InvalidUserException.php | 5 +++ src/Middleware/AuthenticationMiddleware.php | 43 ++++++++++++-------- 6 files changed, 47 insertions(+), 27 deletions(-) create mode 100644 src/Exception/InvalidUserException.php diff --git a/src/Authorize/Bearer/JsonWebTokenDecoder.php b/src/Authorize/Bearer/JsonWebTokenDecoder.php index 039896b..404804d 100644 --- a/src/Authorize/Bearer/JsonWebTokenDecoder.php +++ b/src/Authorize/Bearer/JsonWebTokenDecoder.php @@ -70,11 +70,11 @@ class JsonWebTokenDecoder public function isJWT() : bool { try { - return $this->parse(); + return count(explode('.', $this->encoded)) === 3; + } + catch(\Throwable $t) { + return false; } - catch(\Throwable $t) {} - - return false; } public function getPayload() : array diff --git a/src/Authorize/Header/BearerMethod.php b/src/Authorize/Header/BearerMethod.php index f2b5f20..98a890e 100644 --- a/src/Authorize/Header/BearerMethod.php +++ b/src/Authorize/Header/BearerMethod.php @@ -6,6 +6,7 @@ use Psr\Http\Message\ServerRequestInterface; use Ulmus\User\Authorize\Bearer\JsonWebTokenDecoder; use Ulmus\User\Entity\UserInterface; use Ulmus\User\Lib\Authenticate; +use Ulmus\User\Lib\AuthenticationMethodEnum; class BearerMethod implements MethodInterface { @@ -23,10 +24,15 @@ class BearerMethod implements MethodInterface switch($this->autodetectTokenType()) { case BearerTokenTypeEnum::JsonWebToken: + $this->jwt->decode(); + $payload = $this->jwt->getPayload(); if ( $payload['sub'] ?? false) { - $request = $request->withAttribute('authentication_middleware:user_id', $payload['sub']); + $request = $request + ->withAttribute('authentication_middleware:method', AuthenticationMethodEnum::ForceLogin) + ->withAttribute('authentication_middleware:jwt', $payload) + ->withAttribute('authentication_middleware:user_id', $payload['sub']); } else { throw new \InvalidArgumentException("Given JsonWebToken is missing a 'sub' key (which concords to user id)"); @@ -37,6 +43,8 @@ class BearerMethod implements MethodInterface case BearerTokenTypeEnum::UniqueKey: # @TODO break; + + default: } return $request; diff --git a/src/Authorize/HeaderAuthentication.php b/src/Authorize/HeaderAuthentication.php index 81f161a..74a14dd 100644 --- a/src/Authorize/HeaderAuthentication.php +++ b/src/Authorize/HeaderAuthentication.php @@ -62,7 +62,7 @@ class HeaderAuthentication implements AuthorizeMethodInterface } } }*/ - + return $request->hasHeader('authorization'); } } \ No newline at end of file diff --git a/src/Entity/User.php b/src/Entity/User.php index e5c87c1..469e3dc 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -48,12 +48,12 @@ abstract class User implements UserInterface { #[Field] public string $password; - #[Field\UpdatedAt(name: "updated_at", readonly: true)] - public ? Datetime $updatedAt; - #[Field\CreatedAt(name: "created_at", readonly: true)] public Datetime $createdAt; + #[Field\UpdatedAt(name: "updated_at", readonly: true)] + public ? Datetime $updatedAt; + public bool $logged = false; public function __toString() : string diff --git a/src/Exception/InvalidUserException.php b/src/Exception/InvalidUserException.php new file mode 100644 index 0000000..8472f59 --- /dev/null +++ b/src/Exception/InvalidUserException.php @@ -0,0 +1,5 @@ +getAttribute('authentication_middleware:user_id')) { - $this->authenticator->loadUser($id); - - if ( ! $this->authenticator->user->isLoaded() ) { - throw new \Exception("Given user id do not match with an existing/active user"); - } - } - - switch($request->getAttribute('authentication_middleware:method')) { - case AuthenticationMethodEnum::ForceLogin: - $this->authenticator->login(); - break; - - case AuthenticationMethodEnum::UsernamePassword: - $this->authenticator->authenticate($request->getAttribute('authentication_middleware:username'), $request->getAttribute('authentication_middleware:password')); - break; - } + $this->launchAuthentication($request); } - catch(\Exception $e) { + catch(InvalidUserException $e) { return call_user_func($this->loginFailedResponse, [ 'api.error_message' => $e->getMessage() ]); } return $handler->handle($request); } + + protected function launchAuthentication(ServerRequestInterface $request) : void + { + if (null !== $id = $request->getAttribute('authentication_middleware:user_id')) { + + $this->authenticator->loadUser($id); + + if ( ! $this->authenticator->user->isLoaded() ) { + throw new InvalidUserException("Given user id do not match with an existing/active user"); + } + } + + switch($request->getAttribute('authentication_middleware:method')) { + case AuthenticationMethodEnum::ForceLogin: + $this->authenticator->login(); + break; + + case AuthenticationMethodEnum::UsernamePassword: + $this->authenticator->authenticate($request->getAttribute('authentication_middleware:username'), $request->getAttribute('authentication_middleware:password')); + break; + } + } } From 6a24bcad7657b77d69e2c65a992dc8d00a270a1d Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll Date: Tue, 20 Oct 2026 11:57:30 +0000 Subject: [PATCH 3/3] - Added a new CookieAuthentication middleware --- src/Authorize/Bearer/JsonWebTokenDecoder.php | 2 +- src/Authorize/Bearer/JsonWebTokenValidate.php | 2 +- src/Authorize/CookieAuthentication.php | 33 +++++++++++++++++++ src/Authorize/Header/BearerMethod.php | 15 ++++----- src/Lib/Authenticate.php | 7 ++-- .../CookieAuthenticationMiddleware.php | 30 +++++++++++++++++ 6 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 src/Authorize/CookieAuthentication.php create mode 100644 src/Middleware/CookieAuthenticationMiddleware.php diff --git a/src/Authorize/Bearer/JsonWebTokenDecoder.php b/src/Authorize/Bearer/JsonWebTokenDecoder.php index 404804d..5604684 100644 --- a/src/Authorize/Bearer/JsonWebTokenDecoder.php +++ b/src/Authorize/Bearer/JsonWebTokenDecoder.php @@ -70,7 +70,7 @@ class JsonWebTokenDecoder public function isJWT() : bool { try { - return count(explode('.', $this->encoded)) === 3; + return strlen($this->encoded) > 23 && count(explode('.', $this->encoded)) === 3; } catch(\Throwable $t) { return false; diff --git a/src/Authorize/Bearer/JsonWebTokenValidate.php b/src/Authorize/Bearer/JsonWebTokenValidate.php index 0fb2c7c..25a1bba 100644 --- a/src/Authorize/Bearer/JsonWebTokenValidate.php +++ b/src/Authorize/Bearer/JsonWebTokenValidate.php @@ -53,7 +53,7 @@ abstract class JsonWebTokenValidate if ( ($compare ?? null) !== $decodedSignature) { throw new JsonWebTokenDecodingError( - sprintf("Given signature (%s) do not match computed signature (%s)", $encodedSignature, JsonWebTokenEncoder::base64url_encode($compare)) + sprintf("Given signature (%s) do not match computed signature", $encodedSignature) ); } } diff --git a/src/Authorize/CookieAuthentication.php b/src/Authorize/CookieAuthentication.php new file mode 100644 index 0000000..24952f2 --- /dev/null +++ b/src/Authorize/CookieAuthentication.php @@ -0,0 +1,33 @@ +cookie->get('jwt')); + + if (isset($methodObj)) { + $request = $methodObj->execute($request); + } + + return $request; + } + + public function catchRequest(ServerRequestInterface $request) : bool + { + return $this->cookie->has('jwt'); + } +} \ No newline at end of file diff --git a/src/Authorize/Header/BearerMethod.php b/src/Authorize/Header/BearerMethod.php index 98a890e..004e053 100644 --- a/src/Authorize/Header/BearerMethod.php +++ b/src/Authorize/Header/BearerMethod.php @@ -13,15 +13,11 @@ class BearerMethod implements MethodInterface protected JsonWebTokenDecoder $jwt; public function __construct( - # protected Authenticate $authorize, - # protected UserInterface $user, protected string $token ) {} public function execute(ServerRequestInterface $request) : ServerRequestInterface { - # $this->authorize->user = $this->user; - switch($this->autodetectTokenType()) { case BearerTokenTypeEnum::JsonWebToken: $this->jwt->decode(); @@ -35,7 +31,7 @@ class BearerMethod implements MethodInterface ->withAttribute('authentication_middleware:user_id', $payload['sub']); } else { - throw new \InvalidArgumentException("Given JsonWebToken is missing a 'sub' key (which concords to user id)"); + throw new \InvalidArgumentException("Given JsonWebToken is missing a 'sub' key (which match with a user's id)"); } break; @@ -52,11 +48,14 @@ class BearerMethod implements MethodInterface public function autodetectTokenType() : BearerTokenTypeEnum { - $this->jwt = new JsonWebTokenDecoder($this->token, getenv('LEAN_RANDOM')); + try { + $this->jwt = new JsonWebTokenDecoder($this->token, hash('sha512', getenv('LEAN_RANDOM'))); - if ( $this->jwt->isJWT() ) { - return BearerTokenTypeEnum::JsonWebToken; + if ( $this->jwt->isJWT() ) { + return BearerTokenTypeEnum::JsonWebToken; + } } + catch(\Throwable $ex) {/* Not a JWT */} return BearerTokenTypeEnum::UniqueKey; } diff --git a/src/Lib/Authenticate.php b/src/Lib/Authenticate.php index 5dc1ebd..99cd7ad 100644 --- a/src/Lib/Authenticate.php +++ b/src/Lib/Authenticate.php @@ -37,7 +37,8 @@ class Authenticate { return null; } - public function forgetMe() { + public function forgetMe() : void + { $this->cookie->delete("user.id"); $this->session->destroy(); } @@ -104,8 +105,8 @@ class Authenticate { $this->session->set("user.id", $this->user->id); } - if ( $this->cookie ) { - $this->cookie->set("user.id", $this->user->id); + if ( $this->cookie && ! $this->cookie->has('user.id')) { + $this->cookie->set("user.id", $this->user->id, samesite: "none"); } } diff --git a/src/Middleware/CookieAuthenticationMiddleware.php b/src/Middleware/CookieAuthenticationMiddleware.php new file mode 100644 index 0000000..eeb974c --- /dev/null +++ b/src/Middleware/CookieAuthenticationMiddleware.php @@ -0,0 +1,30 @@ +authenticator->catchRequest($request) ) { + $request = $this->authenticator->connect($request); + } + + return $handler->handle($request); + } +}